20200926のAWSに関する記事は14件です。

AWS FireLensで動かしているFluent Bitに設定した環境変数を使って、他のコンテナのログ設定を行う

What's?

AWS FireLensを使用すると、Amazon ECSやAWS Fargate上で動作するコンテナのログルーターとして機能させることができます。

Firelens の発表 – コンテナログの新たな管理方法

それはいいのですが、AWS FireLensを介した各コンテナのログの出力先は、それぞれのコンテナのlogConfigurationとして設定します。

FireLens構成を使用するタスク定義の作成

ここから、Fluent BitやFluentdの設定が生成されるというわけです。

ですが、この方法だとコンテナごとにログ出力設定が個別に埋め込まれることになり、ログの送信先の設定が同じような場合は管理が煩雑になります。

Fluent Bitでは、設定を環境変数から参照できるようなので、これを利用してみようかなと。

お題

AWS Fargateに、nginxのコンテナを使ったクラスターを構築します。

nginxのログは、AWS FireLensおよびAWS for Fluent Bitを介してAmazon CloudWatch Logsに送信します。

この時、nginxのログ出力設定を、AWS for Fluent Bitに設定した環境変数でコントロールできるか?というのを見てみたいと思います。

環境変数の注入元は、AWS Systems Manager パラメーターストアを使用することにします。

環境は、Terraformで構築します。

環境

$ terraform version
Terraform v0.13.3
+ provider registry.terraform.io/hashicorp/aws v3.8.0


$ aws --version
aws-cli/2.0.52 Python/3.7.3 Linux/4.15.0-112-generic exe/x86_64.ubuntu.20

AWSのクレデンシャルは、環境変数で設定しているものとします。

$ export AWS_ACCESS_KEY_ID=...
$ export AWS_SECRET_ACCESS_KEY=...
$ export AWS_DEFAULT_REGION=ap-northeast-1

AWS Fargateクラスターを構築する

それでは、AWS Fargateクラスターを構築します。

定義はTerraformで書き、VPCやALB、セキュリティグループ等はあとでまとめて載せます。

main.tf

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets                = module.vpc.private_subnets
  nginx_service_security_groups  = [module.nginx_service_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

まずは、Amazon ECSサービスに割り当てる、IAMロールを作成します。

タスク実行用のIAMロールと、タスク用のIAMロールの2つを作成します。

Assume Role。

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

タスク実行用のIAMロール。

data "aws_iam_policy" "ecs_task_execution_role_policy" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_execution_role_policy_document" {
  source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect = "Allow"

    actions = [
      "ssm:GetParameters",
      "kms:Decrypt"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_policy" "ecs_task_execution_policy" {
  name   = "MyEcsTaskExecutionPolicy"
  policy = data.aws_iam_policy_document.ecs_task_execution_role_policy_document.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = aws_iam_policy.ecs_task_execution_policy.arn
}

Amazon Systems Manager パラメーターストアから値を取得するので、AmazonECSTaskExecutionRolePolicyをベースにして権限を追加しています。

data "aws_iam_policy_document" "ecs_task_execution_role_policy_document" {
  source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect = "Allow"

    actions = [
      "ssm:GetParameters",
      "kms:Decrypt"
    ]

    resources = ["*"]
  }
}

タスクに割り当てるIAMロール。AWS for Fluent BitからAmazon CloudWatch Logsにログを送信するための権限を割り当てています。

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name   = "MyEcsTaskPolicy"
  policy = data.aws_iam_policy_document.ecs_task_role_policy_document.json
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_role.name
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}

コンテナのログを保存するAmazon CloudWatch Logsのロググループも作成。

resource "aws_cloudwatch_log_group" "nginx_container_log_group" {
  name = "/fargate/containers/nginx"
}

resource "aws_cloudwatch_log_group" "firelens_container_log_group" {
  name = "/fargate/containers/firelens"
}

AWS FireLensで動かしているFluent Bitのログも、Amazon CloudWatch Logsに送ることにします。

nginx用のコンテナのログ出力先は、Amazon Systems Manager パラメーターストアのSecureStringとして作成しておきましょう。

resource "aws_ssm_parameter" "nginx_log_group" {
  name  = "/config/fargate/containers/nginx/log_group"
  type  = "SecureString"
  value = aws_cloudwatch_log_group.nginx_container_log_group.name
}

resource "aws_ssm_parameter" "nginx_log_stream_prefix" {
  name  = "/config/fargate/containers/nginx/log_stream_prefix"
  type  = "SecureString"
  value = "nginx-"
}

大した情報ではないんですが、今回はSecureStringで。

resource "aws_ecs_cluster" "nginx" {
  name = "nginx-cluster"
}

resource "aws_ecs_task_definition" "nginx" {
  family                   = "nginx-task-definition"
  cpu                      = "512"
  memory                   = "1024"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "nginx:1.19.2",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "$${NGINX_LOG_GROUP}",
          "log_stream_prefix": "$${NGINX_LOG_STREAM_PREFIX}",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/containers/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      },
      "secrets": [
        {
          "name": "NGINX_LOG_GROUP",
          "valueFrom": "/config/fargate/containers/nginx/log_group"
        },
        {
          "name": "NGINX_LOG_STREAM_PREFIX",
          "valueFrom": "/config/fargate/containers/nginx/log_stream_prefix"
        }
      ]
    }
  ]
  JSON
}

resource "aws_ecs_service" "nginx" {
  name             = "nginx-service"
  cluster          = aws_ecs_cluster.nginx.arn
  task_definition  = aws_ecs_task_definition.nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  network_configuration {
    assign_public_ip = false
    security_groups  = local.nginx_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "nginx"
    container_port   = 80
  }
}

先程定義したIAMロールは、Amazon ECSサービスに割り当てます。

resource "aws_ecs_service" "nginx" {
  name             = "nginx-service"
  cluster          = aws_ecs_cluster.nginx.arn
  task_definition  = aws_ecs_task_definition.nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

タスク定義の方ですが、nginxとAWS for Fluent Bitの定義をそれぞれ見ていきます。

nginx側。

    {
      "name": "nginx",
      "image": "nginx:1.19.2",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "$${NGINX_LOG_GROUP}",
          "log_stream_prefix": "$${NGINX_LOG_STREAM_PREFIX}",
          "auto_create_group": "false"
        }
      }
    },

AWS FireLensを介してAmazon CloudWatch Logsにログを送信するわけですが、この送信先ロググループおよびログストリームは環境変数で取得することにします。

      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "$${NGINX_LOG_GROUP}",
          "log_stream_prefix": "$${NGINX_LOG_STREAM_PREFIX}",
          "auto_create_group": "false"
        }
      }

ちなみに、$$となっているのはTerraformでエスケープを行うのが理由なので、Fluent Bit上での環境変数の参照は${NGINX_LOG_GROUP}${NGINX_LOG_STREAM_PREFIX}とするのが正解です。

Variables

つまり、Fluent Bitの定義としては、こういう定義が生成されるはずです。

[OUTPUT]
    Name   cloudwatch
    Match  nginx-firelens*
    region ap-northeast-1
    log_group_name ${NGINX_LOG_GROUP}
    log_stream_prefix ${NGINX_LOG_STREAM_PREFIX}
    auto_create_group false

AWS FireLensで使う、AWS for Fluent Bit側。

    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/containers/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      },
      "secrets": [
        {
          "name": "NGINX_LOG_GROUP",
          "valueFrom": "/config/fargate/containers/nginx/log_group"
        },
        {
          "name": "NGINX_LOG_STREAM_PREFIX",
          "valueFrom": "/config/fargate/containers/nginx/log_stream_prefix"
        }
      ]
    }

環境変数は、AWS for Fluent Bit側に注入します。

      "secrets": [
        {
          "name": "NGINX_LOG_GROUP",
          "valueFrom": "/config/fargate/containers/nginx/log_group"
        },
        {
          "name": "NGINX_LOG_STREAM_PREFIX",
          "valueFrom": "/config/fargate/containers/nginx/log_stream_prefix"
        }
      ]

これで、nginx側で定義したlogConfigurationで環境変数を使いつつ、その値はAWS for Fluent Bit側に設定した環境変数のものが利用されればOKです。

確認

では、環境を構築して

$ terraform apply

確認してみましょう。

AWS for Fluent Bitのログをtailしてみます。

$ aws logs tail --follow /fargate/containers/firelens

しばらく待っていると、起動時のログが出力されます。

2020-09-26T13:45:03.806000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a AWS for Fluent Bit Container Image Version 2.7.0
2020-09-26T13:45:04.464000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a Fluent Bit v1.5.6
2020-09-26T13:45:04.464000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a * Copyright (C) 2019-2020 The Fluent Bit Authors
2020-09-26T13:45:04.464000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a * Copyright (C) 2015-2018 Treasure Data
2020-09-26T13:45:04.464000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a * Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
2020-09-26T13:45:04.464000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a * https://fluentbit.io
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [engine] started (pid=1)
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [storage] version=1.0.5, initializing...
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [storage] in-memory
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [storage] normal synchronization mode, checksum disabled, max_chunks_up=128
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [input:tcp:tcp.0] listening on 127.0.0.1:8877
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [input:forward:forward.1] listening on unix:///var/run/fluent.sock
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [input:forward:forward.2] listening on 127.0.0.1:24224
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter log_group = '/fargate/containers/nginx'"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter log_stream_prefix = 'nginx-'"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter log_stream_name = ''"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter region = 'ap-northeast-1'"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter log_key = ''"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter role_arn = ''"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter new_log_group_tags = ''"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter log_retention_days = '0'"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter endpoint = ''"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter sts_endpoint = ''"
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter credentials_endpoint = "
2020-09-26T13:45:04.468000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a time="2020-09-26T13:45:04Z" level=info msg="[cloudwatch 0] plugin parameter log_format = ''"
2020-09-26T13:45:04.469000+00:00 firelens/log_router/e609bfc9-0347-45cf-b348-a2e296595a1a [2020/09/26 13:45:04] [ info] [sp] stream processor started

AWS for Fluent Bitの環境変数として設定した値が、nginx側のlogConfiguration設定とうまく合わせられたようです。

msg="[cloudwatch 0] plugin parameter log_group = '/fargate/containers/nginx'"
msg="[cloudwatch 0] plugin parameter log_stream_prefix = 'nginx-'"

nginxに、curlでアクセス。

$ curl -I nginx-1423100276.ap-northeast-1.elb.amazonaws.com
HTTP/1.1 200 OK
Date: Sat, 26 Sep 2020 13:47:39 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Server: nginx/1.19.2
Last-Modified: Tue, 11 Aug 2020 14:50:35 GMT
ETag: "5f32b03b-264"
Accept-Ranges: bytes

nginxのログを見てみます。

$ aws logs tail --follow /fargate/containers/nginx

curlのログを含め、nginxのログがAmazon CloudWatch Logsに出力されていることが確認できました。

2020-09-26T13:47:39+00:00 nginx-nginx-firelens-e609bfc9-0347-45cf-b348-a2e296595a1a {"container_id":"e609bfc9-0347-45cf-b348-a2e296595a1a-2531612879","container_name":"nginx","ecs_cluster":"nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:777221001925:task/e609bfc9-0347-45cf-b348-a2e296595a1a","ecs_task_definition":"nginx-task-definition:20","log":"10.0.20.204 - - [26/Sep/2020:13:47:39 +0000] \"HEAD / HTTP/1.1\" 200 0 \"-\" \"curl/7.68.0\" \"124.211.189.238\"","source":"stdout"}
2020-09-26T13:47:43+00:00 nginx-nginx-firelens-e609bfc9-0347-45cf-b348-a2e296595a1a {"container_id":"e609bfc9-0347-45cf-b348-a2e296595a1a-2531612879","container_name":"nginx","ecs_cluster":"nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:777221001925:task/e609bfc9-0347-45cf-b348-a2e296595a1a","ecs_task_definition":"nginx-task-definition:20","log":"10.0.10.249 - - [26/Sep/2020:13:47:43 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"ELB-HealthChecker/2.0\" \"-\"","source":"stdout"}
2020-09-26T13:47:43+00:00 nginx-nginx-firelens-0ae0c358-3bf8-4d2f-bb00-3426f64d76cc {"container_id":"0ae0c358-3bf8-4d2f-bb00-3426f64d76cc-2531612879","container_name":"nginx","ecs_cluster":"nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:777221001925:task/0ae0c358-3bf8-4d2f-bb00-3426f64d76cc","ecs_task_definition":"nginx-task-definition:20","log":"10.0.10.249 - - [26/Sep/2020:13:47:43 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"ELB-HealthChecker/2.0\" \"-\"","source":"stdout"}
2020-09-26T13:47:43+00:00 nginx-nginx-firelens-fe02df02-981e-49ec-b320-4e9f3a42e553 {"container_id":"fe02df02-981e-49ec-b320-4e9f3a42e553-2531612879","container_name":"nginx","ecs_cluster":"nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:777221001925:task/fe02df02-981e-49ec-b320-4e9f3a42e553","ecs_task_definition":"nginx-task-definition:20","log":"10.0.10.249 - - [26/Sep/2020:13:47:43 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"ELB-HealthChecker/2.0\" \"-\"","source":"stdout"}

これで、以下の設定ができたことになります。

  • AWS FireLensを使用し、サイドカーとして動かすAWS for Fluent Bitに環境変数を設定
  • 通常のコンテナは、AWS for Fluent Bitに設定された環境変数をルックアップするようにlogConfigurationを設定

今回確認したかったこととしては、これでOKです。

知識として覚えておきましょう。

VPC〜ALBまで(〜AWS Fargateも)

最後に、省略していたVPCからALBまでの定義を含めた、全体のTerraform定義を載せておきます。

main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.8.0"
    }
  }
}

provider "aws" {
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.54.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  enable_dns_hostnames = true
  enable_dns_support   = true

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.10.0/24", "10.0.20.0/24"]
  private_subnets = ["10.0.30.0/24", "10.0.40.0/24"]

  map_public_ip_on_launch = true

  enable_nat_gateway     = true
  single_nat_gateway     = false
  one_nat_gateway_per_az = true
}

module "load_balancer_sg" {
  source  = "terraform-aws-modules/security-group/aws//modules/http-80"
  version = "3.16.0"

  name   = "load-balancer-sg"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
}

module "nginx_service_sg" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "3.16.0"

  name   = "nginx-service-sg"
  vpc_id = module.vpc.vpc_id

  ingress_with_cidr_blocks = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      description = "nginx-service inbound ports"
      cidr_blocks = "10.0.10.0/24"
    },
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      description = "nginx-service inbound ports"
      cidr_blocks = "10.0.20.0/24"
    }
  ]

  egress_with_cidr_blocks = [
    {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      description = "nginx-service outbound ports"
      cidr_blocks = "0.0.0.0/0"
    }
  ]
}

module "load_balancer" {
  source  = "terraform-aws-modules/alb/aws"
  version = "5.9.0"

  name = "nginx"

  vpc_id             = module.vpc.vpc_id
  load_balancer_type = "application"
  internal           = false

  subnets         = module.vpc.public_subnets
  security_groups = [module.load_balancer_sg.this_security_group_id]

  target_groups = [
    {
      backend_protocol = "HTTP"
      backend_port     = 80
      target_type      = "ip"

      health_check = {
        interval = 20
      }
    }
  ]

  http_tcp_listeners = [
    {
      port     = 80
      protocol = "HTTP"
    }
  ]
}

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets                = module.vpc.private_subnets
  nginx_service_security_groups  = [module.nginx_service_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

data "aws_iam_policy" "ecs_task_execution_role_policy" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_execution_role_policy_document" {
  source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect = "Allow"

    actions = [
      "ssm:GetParameters",
      "kms:Decrypt"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_policy" "ecs_task_execution_policy" {
  name   = "MyEcsTaskExecutionPolicy"
  policy = data.aws_iam_policy_document.ecs_task_execution_role_policy_document.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = aws_iam_policy.ecs_task_execution_policy.arn
}

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name   = "MyEcsTaskPolicy"
  policy = data.aws_iam_policy_document.ecs_task_role_policy_document.json
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_role.name
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}

resource "aws_cloudwatch_log_group" "nginx_container_log_group" {
  name = "/fargate/containers/nginx"
}

resource "aws_cloudwatch_log_group" "firelens_container_log_group" {
  name = "/fargate/containers/firelens"
}

resource "aws_ssm_parameter" "nginx_log_group" {
  name  = "/config/fargate/containers/nginx/log_group"
  type  = "SecureString"
  value = aws_cloudwatch_log_group.nginx_container_log_group.name
}

resource "aws_ssm_parameter" "nginx_log_stream_prefix" {
  name  = "/config/fargate/containers/nginx/log_stream_prefix"
  type  = "SecureString"
  value = "nginx-"
}

resource "aws_ecs_cluster" "nginx" {
  name = "nginx-cluster"
}

resource "aws_ecs_task_definition" "nginx" {
  family                   = "nginx-task-definition"
  cpu                      = "512"
  memory                   = "1024"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "nginx:1.19.2",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "$${NGINX_LOG_GROUP}",
          "log_stream_prefix": "$${NGINX_LOG_STREAM_PREFIX}",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/containers/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      },
      "secrets": [
        {
          "name": "NGINX_LOG_GROUP",
          "valueFrom": "/config/fargate/containers/nginx/log_group"
        },
        {
          "name": "NGINX_LOG_STREAM_PREFIX",
          "valueFrom": "/config/fargate/containers/nginx/log_stream_prefix"
        }
      ]
    }
  ]
  JSON
}

resource "aws_ecs_service" "nginx" {
  name             = "nginx-service"
  cluster          = aws_ecs_cluster.nginx.arn
  task_definition  = aws_ecs_task_definition.nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  network_configuration {
    assign_public_ip = false
    security_groups  = local.nginx_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "nginx"
    container_port   = 80
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

未経験者がWell Architected Frameworkを意識して自分なりにアーキテクチャ設計・構築をしてみた。<WAF、Cloudfront編>

はじめに

この記事は、実務未経験者がロールプレイング形式でアーキテクチャの設計、構築を行うといった記事です。
よろしければ<準備編><アーキテクチャ設計編><VPC、ELB編>もご覧くださいませ。

Cloudfront編

ディストリビューションの作成

Cloudfrontを使用してエッジロケーションからサービスを配信します。
エッジロケーションにキャッシングを行う事でコンテンツを、ユーザーに最も近いサーバーから配信できますので、ネットワークレイテンシの低減オリジンサーバーの負荷軽減を行う事ができます。

ELBなど動的コンテンツオリジンにする場合はどれをキャッシュして、どれをキャッシュさせないのかを検討する必要があります。
今回はとりあえずキャッシングを行わない方針でCloudfrontを導入していきます。

ちなみにキャッシュしない CloudFront は CDN 本来の「エッジサーバにコンテンツをキャッシュし高速に配信する」というメリットはありませんが、それでも導入するメリットは結構あります。

  • HTTP/2 や IPv6、gzip 圧縮といった技術要素を手軽に導入できる
  • DDoS 対策に一定の効果がある
  • パス毎にオリジンを変える事ができる
  • Amazon WAFを手軽に使える
  • 単一リージョンで世界中にサービス展開しているような構成だとレイテンシ改善が期待できる
    • CloudFront のエッジは世界中に 50 以上あり、最も近いエッジサーバに誘導されるから
    • CloudFront のエッジサーバからオリジンサーバへのリクエストは AWS の太いバックボーンを通るから

などなど
実際にCloudfrontでキャッシングさせない方針で導入しているサービスも結構見られます。(Amazon WAFを利用したいからという理由で導入するケースも結構多い気がします)

では実際にCloudfrontの設定を行なっていきましょう!

マネジメントコンールの「Cloudfront」→「Create Distribution」に進んでください。
スクリーンショット 2020-09-25 23.35.20.png

Webの方の「Get Started」をクリック
Cloudfrontは未だに日本後対応してないんですよねー 
結構設定項目が多いので、早く対応して欲しいです

Origin Settings

「Origin Domain Name」は前回作成したALBを指定してください。

「Origin Path」はコンテンツをおいているパスを指定してください。
ApacheのDocumentRootを変更しているので一旦デフォルトのままでいきます

「Origin Protocol Policy」は

  • HTTP Only
    • HTTPのみ許可
  • HTTPS Only
    • HTTPSのみ許可
  • Match Viewer
    • どっちも許可

から選べます。 後ほど変更もできますので、とりあえずMatch Viewerを選んでおきます。

  • Origin Connection Attempts
    • オリジンへの接続を試行する回数
  • Origin Connection Timeout
    • オリジンへの接続を確立しようとするときにCloudFrontが待機する時間
  • Origin Response Timeout
    • CloudFrontがカスタムオリジンからの応答を待機する時間
  • Origin Keep-alive Timeout
    • CloudFrontが接続を閉じる前にカスタムオリジンサーバーとのアイドル接続を維持する時間

の設定項目もとりあえずデフォルトでいいかと思います。
処理に時間かかったり、読み込みが遅いコンテンツの場合はここを変更するといいでしょう

  • Origin Custom Headers Cloudfrontを通過するときに好きなヘッダー、値を追加する事ができます。 アクセス制限をしたりする場合によく使いますね。 後ほど設定しますので一旦スルー

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

Default Cache Behavior Settings

こちらの項目は別途変更を行うので、一旦スルーします

Distribution Settings

「Price Class」
はどこの地域のエッジロケーションを利用するか指定する事が出来ます。
コストを抑えたいときはここを設定してください。

「AWS WAF Web ACL」
後ほどWeb ACLを作成しますので一旦スルー

「Alternate Domain Names」
「SSL Certificate」
ドメインを指定出来ます。後ほど設定しますので一旦スルー

ちなみに設定を行わない場合はデフォルトのドメイン名になります。(デフォルトのドメイン名でもHTTPS化出来ます)
djfaeihfa3eji22.cloudfront.net
みたいな感じですね

残りの項目はCloudfrontのログの設定です。
こちらも後ほど設定していきますので一旦スルーで大丈夫です。

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

Cloudfrontは世界に200以上のエッジロケーションがあるので、一回設定を変更すると結構時間かかります。

Statusが「In Progress」の状態でも変更を行えますので、先ほど作成したディストリビューションを指定してオリジンの詳細設定を行いましょう。

「Behaviors」のタブを選択してください。
現時点では全てのパスが先ほど作成したオリジンにアクセスするように設定されてます。

リストを追加する事ができ、複数ある場合は上から順に適用されます。
Default (*)はどのパスでも当てはまらない場合にアクセスするといったものです。
スクリーンショット 2020-09-26 0.05.00.png

ELBなどで動的コンテンツをオリジンとして指定する場合は
→Default ()はキャッシュさせて、その他のリストはキャッシュさせる
→Default (
)はキャッシュさせず、その他のリストはキャッシュさせる

の2種類の方法を取る事ができます。

キャッシュさせない場合

まずはキャッシュさせない場合はこちらの設定で大丈夫です。

Minimum TTL、Maximum TTL、Default TTLは「Cache Based on Selected Request Headers」をALLに設定すると自動的に0になります。

項目 設定値
Allowed HTTP Methods GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Cache Based on Selected Request Headers ALL
Minimum TTL 0
Maximum TTL 0
Default TTL 0
Forward Cookies ALL
Query String Forwarding and Caching Forward all,cache based on all

その他はデフォルトで大丈夫です。

スクリーンショット 2020-09-26 0.35.46.png

キャッシュさせる場合

項目 設定値
Allowed HTTP Methods GET, HEAD
Cache Based on Selected Request Headers WhiteListにて必要なHeaderを追加(ALLにするとTTLが0に設定されます)
Minimum TTL 0
Maximum TTL お好みのTTL
Default TTL お好みのTTL
Forward Cookies None or whitelist (ALLにするとキャッシュされないので注意)
Query String Forwarding and Caching Forward all,cache based on all

「Cache Based on Selected Request Headers」
でよく追加するヘッダーについて紹介します。

Authorization
管理画面にベーシック認証を入れる時などに必要です。

CloudFront-Forwarded-Proto
Origin側へプロトコルを通知します。

Host
Origin側へアクセス先ホスト名を通知します。

コンテンツによって使用したいヘッダーを追加して下さい。

スクリーンショット 2020-09-26 2.32.44.png

キャッシュさせる場合においても
ログイン系、管理ページ系などはキャッシュしないように設定する必要があります。

スクリーンショット 2020-09-26 2.26.53.png

特定地域からのアクセスをブロックする

今回は日本以外からのアクセスをブロックしてみます

「ディストリビュション」→「Restrictions」→「Edit」を洗濯してください
スクリーンショット 2020-09-26 2.50.39.png

ホワイトリスト、ブラックリスト形式を選択できます。
今回は日本のみを許可したいので、「ホワイトリスト」、「JP--JAPAN」で設定しました。
スクリーンショット 2020-09-26 2.51.04.png

カスタムエラーレスポンスとしてSorry Pageを実装

エラーページを表示させましょう。

準備として、適当なS3バケットを作成、エラーページで使用したいファイルをアップロードしてください。

スクリーンショット 2020-09-26 3.02.48.png

S3をCloudfrontで配信する場合、S3の静的ウェブホスティング、通常バケット、CustomOrigin(Cloudfrontで最初に設定するオリジン)の3種類があります。

  • 通常バケット
    • CloudFront⇔Origin間のHTTPSを自動で設定してくれる
  • S3の静的ウェブホスティング
    • HTTPS化出来ない
  • CustomOrigin
    • HTTPS化する際に自分でSSL証明書を用意する必要がある

などの違いがありますので、今回は通常バケットで用意します。

まずはS3のオリジン登録を行う為に
「Origins and Origin Groups」→「Create Origin」を選択して下さい。

スクリーンショット 2020-09-26 3.21.23.png

先ほどと同じようにS3のドメイン選択、パスの指定を行なって下さい。
また、S3への直接アクセスを制限する為に、OAIを設定しましょう。

「Restrict Bucket Access」をYesに選択すると、OAIの設定項目が追加されます。
「Origin Access Identity」→Create a New Identity
「Grant Read Permissions on Bucket」→Yes, Update Bucket Policy

と設定すると、自動でOAIの作成、バケットポリシーにOAIのIDを追加してくれます。

スクリーンショット 2020-09-26 3.21.00.png

スクリーンショット 2020-09-26 3.27.19.png

後は「Behaviors」の設定を行なって、先ほど作成したオリジンを追加して下さい。
キャッシュに関してはエラーページで設定出来ますので、こちらではキャッシュしない設定で大丈夫です。
スクリーンショット 2020-09-26 3.31.19.png

エラーページを追加する為に「Error Pages」→「Create Custom Error Response」にて設定を行います。

注意点として、長い時間のTTLを設定すると、エラーページから戻らなくなります。
スクリーンショット 2020-09-26 3.33.09.png

補足として、「Behaviors」でキャッシュを行う設定を行なった場合、2XX・3XXに対しては設定したTTLが反映されるのですが、
4XX・5XXに対してはデフォルトで1分間のTTLが設定されています。
もし、エラーに対するTTLの変更を行いたい場合は「Error Pages」より、各エラーに対してのTTLを設定して下さい。

CloudFrontのアクセスログをS3に収集

前回作成したログ専用のバケットを選択して下さい。

プレフィックスを設定できますので今回は「Cloudfront/」を入力します。
スクリーンショット 2020-09-26 20.05.36.png

ログを確認してみましょう。
プレフィックスの設定通り、minamialb/Cloudfrontにログが収集されてます。
スクリーンショット 2020-09-26 20.34.44.png

WAF

CloudFrontからのアクセスのみを許可(オリジンへの直接アクセスを禁止)

現時点では、Cloudfrontからだけではなく、オリジンのALBに対しアクセスが可能となっています。
セキュリティ対策としてWAFをCloudfrontに適用し、アクセス制限を行う場合はALBへの直接アクセスを禁止しましょう。

方法としてはALBを使う方法、WAFを使う方法が一般的ですが、今回はWAFを使ってみます。
現在WAFはWAF v2とWAF Classicがありますが、せっかくなので新しいWAF v2を選択しました。

マネジメントコンソールの「AWS WAF」→「Web ACLs」→「Create web ACL」から作成していきます。
スクリーンショット 2020-09-26 3.48.57.png

「Regional resources」を選択して、ALBが設置されているリージョンを選択します。
その後「Add AWS resources」からWAFを適用したいALBを選択して下さい。
スクリーンショット 2020-09-26 4.43.26.png

WAF v2では様々なマネージドルールを使用できますが、今回は「Add rules」→「Add my own rules and rule groups」からカスタムルールを作成します。

スクリーンショット 2020-09-26 4.44.34.png

カスタムルールの設定を

項目 設定値
Rule type Rule builder
Type Regular rule
If a request matches the statement
Inspect Header
Header field name 任意のヘッダー
Match type Exactly matches strings
String to match 任意の文字列
Text transformation None
Action Allow

の様にして下さい
「Header field name」
「String to match」
に関しては、Cloudfrontの「カスタムオリジンヘッダー」の設定よりヘッダーを追加します。
セキュリティの為、なるべく予測されにくいヘッダーの作成を行って下さい

スクリーンショット 2020-09-26 4.47.30.png

最後に
「Default web ACL action for requests that don't match any rules」をBlockに
カスタムルールの「Action」をAllowにする事で、ルールに対応する場合のみアクセス許可をする事ができます。

スクリーンショット 2020-09-26 4.47.56.png

WAFの方では設定完了ですので、Cloudfrontの設定より、「カスタムオリジンヘッダー」を追加していきましょう。
「Origins」の設定画面から、ALBを選択します。

スクリーンショット 2020-09-26 17.36.24.png

「Origin Custom Headers」より先ほど設定したヘッダーの名前、値を入力して下さい。

注意 今回はテストの為「test」という文字列にしていますが、実際に運用する場合はセキュリティの面から予想されにくいヘッダー名、値にして下さい。

スクリーンショット 2020-09-26 17.36.39.png

実際にALBのDNS名からアクセスすると「403 forbidden」が表示されるかと思います。

Web ACLの設定

最後に

参考
「キャッシュさせないCloudFrontディストリビューションを設定してみた(CloudFormationテンプレート付)」
https://dev.classmethod.jp/articles/create-no-cached-cloudfront-distribution/

「Amazon Athena で CloudFront のアクセスログを集計する」
https://dev.classmethod.jp/articles/athena-cloudfront-log-activity/

「Athena で CloudFront ログ解析」
https://qiita.com/ytanaka3/items/ad5e7d96bc425ff4c843

「キャッシュしない CloudFront とそのメリット・デメリット」
https://blog.manabusakai.com/2016/11/no-cache-cloudfront/

「AWS Cloudfrontの地域制限機能を使って特定地域からのアクセスを遮断する」
https://beyondjapan.com/blog/2016/09/aws-cloudfront-specificarea-blocking/

「CloudFrontを利用してオリジンサーバー障害時にS3上のコンテンツを表示する」
https://dev.classmethod.jp/articles/cloudfront-sorry-s3/

「CloudFrontのCustom Error Responseを利用して、S3上にあるSorryページを表示する」
https://dev.classmethod.jp/articles/cloudfront_customerrorresponse_s3/

「CloudFront および Lambda@Edge のメトリクスの表示」
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/viewing-cloudfront-metrics.html

「AWS WAFを利用してCloudFrontのELBオリジンへ直接アクセスを制限してみた」
https://dev.classmethod.jp/articles/restrict-elb-origin-awswaf/

「いますぐ使う CloudFront」
https://qiita.com/sasasin/items/0f0ec1a90af6295589f9

「AWS CloudFrontで極力キャッシュさせたくない時の話」
https://qiita.com/shogomuranushi/items/a2367350ea54a8f41257![スクリーンショット 2020-09-26 2.50.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666664/6e7e3508-599c-1e38-0172-7b0de1e99c68.png)
te

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

Well Architected Frameworkを意識して自分なりにアーキテクチャ設計・構築をしてみた。<WAF、Cloudfront編>

はじめに

この記事は、実務未経験者がロールプレイング形式でアーキテクチャの設計、構築を行うといった記事です。
よろしければ<準備編><アーキテクチャ設計編><VPC、ELB編>もご覧くださいませ。

Cloudfront編

ディストリビューションの作成

Cloudfrontを使用してエッジロケーションからサービスを配信します。
エッジロケーションにキャッシングを行う事でコンテンツを、ユーザーに最も近いサーバーから配信できますので、ネットワークレイテンシの低減オリジンサーバーの負荷軽減を行う事ができます。

ELBなど動的コンテンツオリジンにする場合はどれをキャッシュして、どれをキャッシュさせないのかを検討する必要があります。
今回はとりあえずキャッシングを行わない方針でCloudfrontを導入していきます。

ちなみにキャッシュしない CloudFront は CDN 本来の「エッジサーバにコンテンツをキャッシュし高速に配信する」というメリットはありませんが、それでも導入するメリットは結構あります。

  • HTTP/2 や IPv6、gzip 圧縮といった技術要素を手軽に導入できる
  • DDoS 対策に一定の効果がある
  • パス毎にオリジンを変える事ができる
  • Amazon WAFを手軽に使える
  • 単一リージョンで世界中にサービス展開しているような構成だとレイテンシ改善が期待できる
    • CloudFront のエッジは世界中に 50 以上あり、最も近いエッジサーバに誘導されるから
    • CloudFront のエッジサーバからオリジンサーバへのリクエストは AWS の太いバックボーンを通るから

などなど
実際にCloudfrontでキャッシングさせない方針で導入しているサービスも結構見られます。(Amazon WAFを利用したいからという理由で導入するケースも結構多い気がします)

では実際にCloudfrontの設定を行なっていきましょう!

マネジメントコンールの「Cloudfront」→「Create Distribution」に進んでください。
スクリーンショット 2020-09-25 23.35.20.png

Webの方の「Get Started」をクリック
Cloudfrontは未だに日本後対応してないんですよねー 
結構設定項目が多いので、早く対応して欲しいです

Origin Settings

「Origin Domain Name」は前回作成したALBを指定してください。

「Origin Path」はコンテンツをおいているパスを指定してください。
ApacheのDocumentRootを変更しているので一旦デフォルトのままでいきます

「Origin Protocol Policy」は

  • HTTP Only
    • HTTPのみ許可
  • HTTPS Only
    • HTTPSのみ許可
  • Match Viewer
    • どっちも許可

から選べます。 後ほど変更もできますので、とりあえずMatch Viewerを選んでおきます。

  • Origin Connection Attempts
    • オリジンへの接続を試行する回数
  • Origin Connection Timeout
    • オリジンへの接続を確立しようとするときにCloudFrontが待機する時間
  • Origin Response Timeout
    • CloudFrontがカスタムオリジンからの応答を待機する時間
  • Origin Keep-alive Timeout
    • CloudFrontが接続を閉じる前にカスタムオリジンサーバーとのアイドル接続を維持する時間

の設定項目もとりあえずデフォルトでいいかと思います。
処理に時間かかったり、読み込みが遅いコンテンツの場合はここを変更するといいでしょう

  • Origin Custom Headers Cloudfrontを通過するときに好きなヘッダー、値を追加する事ができます。 アクセス制限をしたりする場合によく使いますね。 後ほど設定しますので一旦スルー

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

Default Cache Behavior Settings

こちらの項目は別途変更を行うので、一旦スルーします

Distribution Settings

「Price Class」
はどこの地域のエッジロケーションを利用するか指定する事が出来ます。
コストを抑えたいときはここを設定してください。

「AWS WAF Web ACL」
後ほどWeb ACLを作成しますので一旦スルー

「Alternate Domain Names」
「SSL Certificate」
ドメインを指定出来ます。後ほど設定しますので一旦スルー

ちなみに設定を行わない場合はデフォルトのドメイン名になります。(デフォルトのドメイン名でもHTTPS化出来ます)
djfaeihfa3eji22.cloudfront.net
みたいな感じですね

残りの項目はCloudfrontのログの設定です。
こちらも後ほど設定していきますので一旦スルーで大丈夫です。

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

Cloudfrontは世界に200以上のエッジロケーションがあるので、一回設定を変更すると結構時間かかります。

Behaviorsの設定

Statusが「In Progress」の状態でも変更を行えますので、先ほど作成したディストリビューションを指定してオリジンの詳細設定を行いましょう。

「Behaviors」のタブを選択してください。
現時点では全てのパスが先ほど作成したオリジンにアクセスするように設定されてます。

リストを追加する事ができ、複数ある場合は上から順に適用されます。
Default (*)はどのパスでも当てはまらない場合にアクセスするといったものです。
スクリーンショット 2020-09-26 0.05.00.png

ELBなどで動的コンテンツをオリジンとして指定する場合は
→Default ()はキャッシュさせて、その他のリストはキャッシュさせる
→Default (
)はキャッシュさせず、その他のリストはキャッシュさせる

の2種類の方法を取る事ができます。

キャッシュさせない場合

まずはキャッシュさせない場合はこちらの設定で大丈夫です。

Minimum TTL、Maximum TTL、Default TTLは「Cache Based on Selected Request Headers」をALLに設定すると自動的に0になります。

項目 設定値
Allowed HTTP Methods GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Cache Based on Selected Request Headers ALL
Minimum TTL 0
Maximum TTL 0
Default TTL 0
Forward Cookies ALL
Query String Forwarding and Caching Forward all,cache based on all

その他はデフォルトで大丈夫です。

スクリーンショット 2020-09-26 0.35.46.png

キャッシュさせる場合

項目 設定値
Allowed HTTP Methods GET, HEAD
Cache Based on Selected Request Headers WhiteListにて必要なHeaderを追加(ALLにするとTTLが0に設定されます)
Minimum TTL 0
Maximum TTL お好みのTTL
Default TTL お好みのTTL
Forward Cookies None or whitelist (ALLにするとキャッシュされないので注意)
Query String Forwarding and Caching Forward all,cache based on all

「Cache Based on Selected Request Headers」
でよく追加するヘッダーについて紹介します。

Authorization
管理画面にベーシック認証を入れる時などに必要です。

CloudFront-Forwarded-Proto
Origin側へプロトコルを通知します。

Host
Origin側へアクセス先ホスト名を通知します。

コンテンツによって使用したいヘッダーを追加して下さい。

スクリーンショット 2020-09-26 2.32.44.png

キャッシュさせる場合においても
ログイン系、管理ページ系などはキャッシュしないように設定する必要があります。

スクリーンショット 2020-09-26 2.26.53.png

特定地域からのアクセスをブロックする

今回は日本以外からのアクセスをブロックしてみます

「ディストリビュション」→「Restrictions」→「Edit」を洗濯してください
スクリーンショット 2020-09-26 2.50.39.png

ホワイトリスト、ブラックリスト形式を選択できます。
今回は日本のみを許可したいので、「ホワイトリスト」、「JP--JAPAN」で設定しました。
スクリーンショット 2020-09-26 2.51.04.png

カスタムエラーレスポンスとしてSorry Pageを実装

エラーページを表示させましょう。

準備として、適当なS3バケットを作成、エラーページで使用したいファイルをアップロードしてください。

スクリーンショット 2020-09-26 3.02.48.png

S3をCloudfrontで配信する場合、S3の静的ウェブホスティング、通常バケット、CustomOrigin(Cloudfrontで最初に設定するオリジン)の3種類があります。

  • 通常バケット
    • CloudFront⇔Origin間のHTTPSを自動で設定してくれる
  • S3の静的ウェブホスティング
    • HTTPS化出来ない
  • CustomOrigin
    • HTTPS化する際に自分でSSL証明書を用意する必要がある

などの違いがありますので、今回は通常バケットで用意します。

まずはS3のオリジン登録を行う為に
「Origins and Origin Groups」→「Create Origin」を選択して下さい。

スクリーンショット 2020-09-26 3.21.23.png

先ほどと同じようにS3のドメイン選択、パスの指定を行なって下さい。
また、S3への直接アクセスを制限する為に、OAIを設定しましょう。

「Restrict Bucket Access」をYesに選択すると、OAIの設定項目が追加されます。
「Origin Access Identity」→Create a New Identity
「Grant Read Permissions on Bucket」→Yes, Update Bucket Policy

と設定すると、自動でOAIの作成、バケットポリシーにOAIのIDを追加してくれます。

スクリーンショット 2020-09-26 3.21.00.png

スクリーンショット 2020-09-26 3.27.19.png

後は「Behaviors」の設定を行なって、先ほど作成したオリジンを追加して下さい。
キャッシュに関してはエラーページで設定出来ますので、こちらではキャッシュしない設定で大丈夫です。
スクリーンショット 2020-09-26 3.31.19.png

エラーページを追加する為に「Error Pages」→「Create Custom Error Response」にて設定を行います。

注意点として、長い時間のTTLを設定すると、エラーページから戻らなくなります。
スクリーンショット 2020-09-26 3.33.09.png

補足として、「Behaviors」でキャッシュを行う設定を行なった場合、2XX・3XXに対しては設定したTTLが反映されるのですが、
4XX・5XXに対してはデフォルトで1分間のTTLが設定されています。
もし、エラーに対するTTLの変更を行いたい場合は「Error Pages」より、各エラーに対してのTTLを設定して下さい。

CloudFrontのアクセスログをS3に収集

前回作成したログ専用のバケットを選択して下さい。

プレフィックスを設定できますので今回は「Cloudfront/」を入力します。
スクリーンショット 2020-09-26 20.05.36.png

ログを確認してみましょう。
プレフィックスの設定通り、minamialb/Cloudfrontにログが収集されてます。
スクリーンショット 2020-09-26 20.34.44.png

WAF

CloudFrontからのアクセスのみを許可(オリジンへの直接アクセスを禁止)

現時点では、Cloudfrontからだけではなく、オリジンのALBに対しアクセスが可能となっています。
セキュリティ対策としてWAFをCloudfrontに適用し、アクセス制限を行う場合はALBへの直接アクセスを禁止しましょう。

方法としてはALBを使う方法、WAFを使う方法が一般的ですが、今回はWAFを使ってみます。
現在WAFはWAF v2とWAF Classicがありますが、せっかくなので新しいWAF v2を選択しました。

マネジメントコンソールの「AWS WAF」→「Web ACLs」→「Create web ACL」から作成していきます。
スクリーンショット 2020-09-26 3.48.57.png

「Regional resources」を選択して、ALBが設置されているリージョンを選択します。
その後「Add AWS resources」からWAFを適用したいALBを選択して下さい。
スクリーンショット 2020-09-26 4.43.26.png

WAF v2では様々なマネージドルールを使用できますが、今回は「Add rules」→「Add my own rules and rule groups」からカスタムルールを作成します。

スクリーンショット 2020-09-26 4.44.34.png

カスタムルールの設定を

項目 設定値
Rule type Rule builder
Type Regular rule
If a request matches the statement
Inspect Header
Header field name 任意のヘッダー
Match type Exactly matches strings
String to match 任意の文字列
Text transformation None
Action Allow

の様にして下さい
「Header field name」
「String to match」
に関しては、Cloudfrontの「カスタムオリジンヘッダー」の設定よりヘッダーを追加します。
セキュリティの為、なるべく予測されにくいヘッダーの作成を行って下さい

スクリーンショット 2020-09-26 4.47.30.png

最後に
「Default web ACL action for requests that don't match any rules」をBlockに
カスタムルールの「Action」をAllowにする事で、ルールに対応する場合のみアクセス許可をする事ができます。

スクリーンショット 2020-09-26 4.47.56.png

WAFの方では設定完了ですので、Cloudfrontの設定より、「カスタムオリジンヘッダー」を追加していきましょう。
「Origins」の設定画面から、ALBを選択します。

スクリーンショット 2020-09-26 17.36.24.png

「Origin Custom Headers」より先ほど設定したヘッダーの名前、値を入力して下さい。

注意 今回はテストの為「test」という文字列にしていますが、実際に運用する場合はセキュリティの面から予想されにくいヘッダー名、値にして下さい。

スクリーンショット 2020-09-26 17.36.39.png

実際にALBのDNS名からアクセスすると「403 forbidden」が表示されるかと思います。

スクリーンショット 2020-09-26 23.56.45.png

Web ACLの設定

スクリーンショット 2020-09-26 23.39.06.png

スクリーンショット 2020-09-26 23.40.46.png

スクリーンショット 2020-09-26 23.41.34.png

スクリーンショット 2020-09-26 23.41.44.png

スクリーンショット 2020-09-26 23.42.01.png

最後に

参考
「キャッシュさせないCloudFrontディストリビューションを設定してみた(CloudFormationテンプレート付)」
https://dev.classmethod.jp/articles/create-no-cached-cloudfront-distribution/

「Amazon Athena で CloudFront のアクセスログを集計する」
https://dev.classmethod.jp/articles/athena-cloudfront-log-activity/

「Athena で CloudFront ログ解析」
https://qiita.com/ytanaka3/items/ad5e7d96bc425ff4c843

「キャッシュしない CloudFront とそのメリット・デメリット」
https://blog.manabusakai.com/2016/11/no-cache-cloudfront/

「AWS Cloudfrontの地域制限機能を使って特定地域からのアクセスを遮断する」
https://beyondjapan.com/blog/2016/09/aws-cloudfront-specificarea-blocking/

「CloudFrontを利用してオリジンサーバー障害時にS3上のコンテンツを表示する」
https://dev.classmethod.jp/articles/cloudfront-sorry-s3/

「CloudFrontのCustom Error Responseを利用して、S3上にあるSorryページを表示する」
https://dev.classmethod.jp/articles/cloudfront_customerrorresponse_s3/

「CloudFront および Lambda@Edge のメトリクスの表示」
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/viewing-cloudfront-metrics.html

「AWS WAFを利用してCloudFrontのELBオリジンへ直接アクセスを制限してみた」
https://dev.classmethod.jp/articles/restrict-elb-origin-awswaf/

「いますぐ使う CloudFront」
https://qiita.com/sasasin/items/0f0ec1a90af6295589f9

「AWS CloudFrontで極力キャッシュさせたくない時の話」
https://qiita.com/shogomuranushi/items/a2367350ea54a8f41257![スクリーンショット 2020-09-26 2.50.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666664/6e7e3508-599c-1e38-0172-7b0de1e99c68.png)

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

中間CAの証明書期限切れ時のAmazon API Gatewayのトラストストアの更新を試してみた。

はじめに

Amazon API GatewayのmTLS機能リリースに絡めて、以下の3つの記事を書きましたが、書いている中で私がTLSの証明書や、その検証がどう行われているのか、また、CAの証明書の更新期限が近づいた時の運用がどうなるのか?等、PKIの基本がわかっている方なら、あっさりと「そこ、なやむの?」といわれそうな部分ではありますが、色々復習しながら検証を進めました。

サマリ

  • ルートCAや中間CAの秘密鍵が再生成されなければ、証明書更新(期限切れに備えた更新を想定)はクライアント証明書による接続に影響なし
  • 期限切れしたCA証明書に署名されたクライアント証明書は接続できないが、CA証明書を更新しトラストストアに反映すれば接続可能
  • 秘密鍵を更新したCA証明書をトラストストアに反映後は旧CA秘密鍵により署名されたクライアント証明書は接続不可

おさらい

一連の記事を書く中で、OpenSSLコマンドを使っていくつかおさらいを実施ししました。特にぼんやりしていたのは、CA証明書の更新、秘密鍵の更新でした。ぼんやりとは理解していましたが、仕様等をちゃんと理解していなかった部分もあり、OpenSSLコマンドを利用して色々確認しました。

今回想定したこと

「ルートCAや中間CAの証明書の有効期限が切れた時、API Gateway側でmTLSのトラストストアとしてその証明書を利用してる場合、つまり、クライアント証明書が、その中間CAによって生成されていた場合どうなるのだろう?」という疑問から以下、登場人物とシチュエーションを想定し検証をしました。

登場人物

  1. ルートCA 6か月後に証明書期限切れ
  2. 中間CA1 3か月後に証明書期限切れ
  3. 中間CA2 3か月後に証明書期限切れ
  4. 中間CA3 3か月後に証明書期限切れ。秘密鍵の漏洩が発覚
  5. 中間CA4 既に証明書期限切れ
  6. WebAPI提供者 中間CA/ルートCAの証明書をトラストストアに格納。Amazon API Gatewayで提供
  7. クライアント 上記CAからクライアント証明書を発行。WebAPIに接続。

シチュエーション

  1. ルートCAは6か月後の証明書期限切れに備えて、1時間前に証明書を更新し公開した
  2. 中間CA2は3か月後の証明書期限切れに備えて、1時間前に証明書を更新した
  3. 中間CA3は鍵が漏洩したので、1時間前に証明書と秘密鍵を更新した。
  4. 中間CA4は期限切れを起こしてしまったので、1時間前に証明書を更新した。
  5. 中間CA2,3は証明書更新後に既に新規にクライアント証明書を発行している。

上記において現在、Amazon API GatewayのmTLS構成でクライアントは接続しているが、中間CA4が既に接続エラーを起こしていること、秘密鍵漏洩を中間CA3で発生しており、早急に対応が必要となった。

お断り:今回CRLの運用は実施していません。

確認したいポイント

1. API Gatewayのトラストストア更新前でも、新規に発行されたCA証明書を利用して生成されたクライアント証明書が接続できるか否か
2. CA証明書が更新されても秘密鍵が更新されていなければ以前のCA証明書で生成された、有効期間内のクライアント証明書が継続利用できること
3. ついでに確認したいこと(期限切れの証明書をAPI Gatewayには登録できるのか?)
4. ついでに確認したいこと(既に登録済みの期限が切れの中間証明書をAPI Gatewayは検証でどう扱うのか?)

準備するもの

最初にOpenSSLで以下を準備

  1. ルートCA用の秘密鍵と証明書 更新あり
  2. 中間CA1用の秘密鍵と証明書: 更新なし
  3. 中間CA2用の秘密鍵と証明書: 更新あり
  4. 中間CA3用の秘密鍵と証明書: 秘密鍵更新あり
  5. 中間CA4用の秘密鍵と証明書: 期限切れ
  6. 中間CA1が発行するクライアント証明書
  7. 中間CA2が発行するクライアント証明書
  8. 中間CA3が発行するクライアント証明書
  9. 中間CA4が発行するクライアント証明書

途中で更新するもの

  1. ルートCAの証明書
  2. 中間CA2の証明書
  3. 中間CA3の秘密鍵/証明書

途中で追加するもの

  1. 中間CA2のクライアント証明書(CA更新後)
  2. 中間CA3のクライアント証明書(CA秘密鍵、CA証明書更新後)

上記準備するものについては、最初にOpenSSLを利用して全て作成しました。

作成コマンド

#Root CA Private Key
openssl genrsa -out rootCA.key 2048
#Root CA Self-sign Certificate
openssl req -x509 -new -key rootCA.key -sha256 -days 180 -out rootCA.pem  -subj "/CN=Private ROOT CA CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"

#Intermediate CA Private Key
openssl genrsa -out intermediateCA1.key 2048
openssl genrsa -out intermediateCA2.key 2048
openssl genrsa -out intermediateCA3.key 2048
openssl genrsa -out intermediateCA4.key 2048

#Intermediate CA CSR
openssl req -new -key intermediateCA1.key -out intermediateCA1.csr  -subj "/CN=Private Inter CA1 CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"
openssl req -new -key intermediateCA2.key -out intermediateCA2.csr  -subj "/CN=Private Inter CA2 CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"
openssl req -new -key intermediateCA3.key -out intermediateCA3.csr  -subj "/CN=Private Inter CA3 CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"
openssl req -new -key intermediateCA4.key -out intermediateCA4.csr  -subj "/CN=Private Inter CA4 CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"


#Intermediate CA Certificate signed by Root CA
openssl x509 -req -in intermediateCA1.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA1.pem -days 90
openssl x509 -req -in intermediateCA2.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA2.pem -days 90
openssl x509 -req -in intermediateCA3.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA3.pem -days 90
openssl x509 -req -in intermediateCA4.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA4.pem -days 0


#Client Secret Key
openssl genrsa -out client1.key 2048
openssl genrsa -out client2.key 2048
openssl genrsa -out client3.key 2048
openssl genrsa -out client4.key 2048


#Client CSR
openssl req -new -key client1.key -out client1.csr  -subj "/CN=Client1"
openssl req -new -key client2.key -out client2.csr  -subj "/CN=Client2"
openssl req -new -key client3.key -out client3.csr  -subj "/CN=Client3"
openssl req -new -key client4.key -out client4.csr  -subj "/CN=Clinet4"

#Client Certificate signed by Intermediate CA
openssl x509 -req -in client1.csr -CA intermediateCA1.pem -CAkey intermediateCA1.key -CAcreateserial -sha256  -out client1.pem -days 90
openssl x509 -req -in client2.csr -CA intermediateCA2.pem -CAkey intermediateCA2.key -CAcreateserial -sha256  -out client2.pem -days 90
openssl x509 -req -in client3.csr -CA intermediateCA3.pem -CAkey intermediateCA3.key -CAcreateserial -sha256  -out client3.pem -days 90
openssl x509 -req -in client4.csr -CA intermediateCA4.pem -CAkey intermediateCA4.key -CAcreateserial -sha256  -out client4.pem -days 1

トラストストアの置き換え(新規作成でもよい)

既に以前の記事でAWS の ACM Private CAを利用した証明書発行ならびにAPI Gatewayのトラストストア登録をしていましたので、その時に設定した中間CAやルートCAの証明書がトラストストアに登録されています。それを以下のコマンドで作成したトラストストアに置き換えます。

catで実行すると改行がはいらず形式が崩れるので awk 1を利用しました。

awk 1 rootCA.pem \
      intermediateCA1.pem  \
      intermediateCA2.pem  \
      intermediateCA3.pem  \
      intermediateCA4.pem  \
      > truststore

これをS3 にアップロードしてAPI Gatewayのトラストストア設定に反映するだけ。

# Upload してVersionIDを取得。
VersionId=$(aws s3api put-object --bucket mtlstest-truststores3bucket-6rdlcmrisggk --key truststore --body  truststore | jq -r '.VersionId' )

# VersionIDを指定してトラストストアを更新。
aws apigatewayv2 update-domain-name \
  --domain-name api.xxxx.xxxxx\
  --domain-name-configurations CertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/123456-626f-48c5-b10b-fc51b4545730,EndpointType=REGIONAL \
  --mutual-tls-authentication TruststoreUri=s3://mtlstest-truststores3bucket-6rdlcmrisggk/truststore,TruststoreVersion=$VersionId

サイド、以下のコマンドで接続確認をしたところ、Client1,Client2,Client3は接続できましたが、Client4はCA証明書が有効期限切れのため接続できないようでした。

curl --key  client1.key --cert client1.pem   https://api.XXXX.XXXX/hoge/auth
curl --key  client2.key --cert client2.pem   https://api.XXXX.XXXX/hoge/auth
curl --key  client3.key --cert client3.pem   https://api.XXXX.XXXX/hoge/auth
curl --key  client4.key --cert client4.pem   https://api.XXXX.XXXX/hoge/auth

念のため、Client4の証明書を確認しましたが以下の通りでした。

date; openssl x509 -in client4.pem -text -noout                                                                                                                                                                                                                                                                            
Sat Sep 26 11:32:03 UTC 2020
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            71:c6:98:60:13:a4:42:fa:42:4f:af:30:10:5f:01:f2:0e:41:2b:ad
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Private Inter CA4 CN, O = Sample Org, OU = Sample Ou, L = Meguro, ST = Tokyo, C = JP
        Validity
            Not Before: Sep 26 11:31:35 2020 GMT
            Not After : Sep 27 11:31:35 2020 GMT
        Subject: CN = Clinet4
        Subject Public Key Info:

現在時刻をdateコマンドで最初に表示しています。ご覧いただいた通り、クライアント証明書はNot Before とNot Afterの時刻の間にあり、有効な範囲内ですが、CA証明書が有効期間内ではありません。以下、CA証明書の情報の抜粋です。

date; openssl x509 -in intermediateCA4.pem -text -noout                                                                                                                                                                                                                                                                            
Sat Sep 26 11:32:29 UTC 2020
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            76:9c:ad:af:75:af:6f:15:70:ac:ba:d0:6e:d8:cd:34:78:96:4a:08
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Private ROOT CA CN, O = Sample Org, OU = Sample Ou, L = Meguro, ST = Tokyo, C = JP
        Validity
            Not Before: Sep 26 11:31:34 2020 GMT
            Not After : Sep 26 11:31:34 2020 GMT
        Subject: CN = Private Inter CA4 CN, O = Sample Org, OU = Sample Ou, L = Meguro, ST = Tokyo, C = JP
        Subject Public Key Info:

検証開始

期限切れCAの証明書更新

期限切れのCA証明書を利用したクライアント認証は成功しないため、ここではClient4による接続を回復するために期限切れCAの証明書更新を更新します。

以下のコマンドで期限を延ばして再度証明書を生成し、トラストストアを更新しました。前回の記事でも書きましたが、Amazon API Gatewayでは、クライアント証明書に署名をした中間CAだけでなく、上位のCA(ルートCAまでのチェーン)の公開鍵証明書をトラストストアに格納する必要がありますが、同一サブジェクト名の証明書は1つしか置けません。したがって、期限切れ証明書を取り除いた有効なCA証明書を結合(今回はawkで結合しています)して配置し、設定を更新しました。
運用を考えると、ルートCA、中間CAの有効な証明書を個別に保持しそれを必要な都度、結合して登録するのが良いのではないでしょうか?(都度、中身をけして、追加するという作業はミスと有無と思います)

#Intermediate CA Certificate signed by Root CA
openssl x509 -req -in intermediateCA4.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA4Updated.pem -days 360


awk 1 rootCA.pem \
    intermediateCA1.pem \
    intermediateCA2.pem \
    intermediateCA3.pem \
    intermediateCA4Updated.pem \
    > truststore

VersionId=$(aws s3api put-object --bucket mtlstest-truststores3bucket-6rdlcmrisggk --key truststore --body  truststore | jq -r '.VersionId' )

aws apigatewayv2 update-domain-name \
              --domain-name api.horiba.work \
              --domain-name-configurations CertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/12345678-626f-48c5-b10b-fc51b4545730,EndpointType=REGIONAL \
              --mutual-tls-authentication TruststoreUri=s3://mtlstest-truststores3bucket-6rdlcmrisggk/truststore,TruststoreVersion=$VersionId


設定が反映された後、動作確認をしたところ、Client4で接続が確認できました。

期限切れ間近の証明書の更新

ここでの確認点は、CA証明書更新後に新たに生成されたクライアント証明書は、トラストストア更新前でも接続可能か?という点です。
既にClient4の検証を通じて、CA証明書が更新されても過去に生成されたクライアント証明書で接続できることは確認しました。ここでは逆を確認したいと思います。
ただ、秘密鍵は更新せず、証明書の更新のみなので影響ないことを念のため示す形になります。
ここでは

  • rootCA
  • Intermidediate2

を更新し、新しい証明書を利用したクライアント証明書の作成を実施したいと思います。

#Update Root CA Self-sign Certificate
openssl req -x509 -new -key rootCA.key -sha256 -days 365 -out rootCAUpdated.pem  -subj "/CN=Private ROOT CA CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"

#Update Intermediate CA Certificate signed by Root CA
openssl x509 -req -in intermediateCA2.csr -CA rootCAUpdated.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA2Updated.pem -days 180

上記は、ルートおよび中間CAを更新した状態です。ただし、まだ、API Gatewayには反映していません。反映前に以下のコマンドで新しく秘密鍵を作り、証明書を発行し、接続確認をしてみました。過去の鍵も含め、以下の5つのCurlコマンドは全て接続に成功しました。
署名は秘密鍵を利用して行っているため、秘密鍵を変えない限り、影響は受けないのだと理解しています。

#generate new Client 
openssl genrsa -out client2new.key 2048

openssl req -new -key client2new.key -out client2new.csr  -subj "/CN=Client2new"
#Client Certificate signed by Intermediate CA
openssl x509 -req -in client2new.csr -CA intermediateCA2Updated.pem -CAkey intermediateCA2.key -CAcreateserial -sha256  -out client2new.pem -days 90


curl --key  client1.key --cert client1.pem   https://api.xxxxx.xxx/hoge/auth
curl --key  client2.key --cert client2.pem   https://api.xxxxx.xxx/hoge/auth
curl --key  client3.key --cert client3.pem   https://api.xxxxx.xxx/hoge/auth
curl --key  client4.key --cert client4.pem   https://api.xxxxx.xxx/hoge/auth

curl --key  client2new.key --cert client2new.pem   https://api.xxxx.xxx/hoge/auth

さて、トラストストア内のルートCAおよび、中間CAのintermediateCA2の証明書を最新の証明書に更新するために以下のコマンドを実施しておきます。

awk 1 rootCAUpdated.pem intermediateCA1.pem intermediateCA2Updated.pem intermediateCA3.pem intermediateCA4Updated.pem > truststore
VersionId=$(aws s3api put-object --bucket mtlstest-truststores3bucket-6rdlcmrisggk --key truststore --body  truststore | jq -r '.VersionId' )

aws apigatewayv2 update-domain-name \
              --domain-name api.XXXX.XXXX \
              --domain-name-configurations CertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/12345678-626f-48c5-b10b-fc51b4545730,EndpointType=REGIONAL \
              --mutual-tls-authentication TruststoreUri=s3://mtlstest-truststores3bucket-6rdlcmrisggk/truststore,TruststoreVersion=$VersionId

上記設定を実施後も過去に発行されたクライアント証明書で問題なく接続できました。公開鍵証明書が更新されても期限切れでなければ動作することを確認できました。さて問題はこの次です。中間CAの秘密鍵が漏洩した場合は??です。

CAの秘密鍵漏洩による、秘密鍵の更新とCA証明書更新

今までは公開鍵のペアとなる秘密鍵は更新されなかったので期限切れを除いて影響を受けることなく更新ができました。ここでは、有効期限は切れていないが秘密鍵が更新、つまり、公開鍵が更新される場合の動きを確認したいと思います。
まず、以下のコマンドでIntermediateCA3の新秘密鍵の生成、新CSRの生成、そして証明書の生成を行いました。また、合わせて、新証明書を利用した新クライアント証明書の生成も行いました。

#Intermediate CA Private Key
openssl genrsa -out intermediateCA3new.key 2048

#Intermediate CA CSR
openssl req -new -key intermediateCA3new.key -out intermediateCA3new.csr  -subj "/CN=Private Inter CA3 CN/O=Sample Org/OU=Sample Ou/L=Meguro/ST=Tokyo/C=JP"

#Intermediate CA Certificate signed by Root CA
openssl x509 -req -in intermediateCA3new.csr -CA rootCAUpdated.pem -CAkey rootCA.key -CAcreateserial -sha256  -out intermediateCA3new.pem -days 90
openssl genrsa -out client3new.key 2048


#Client CSR
openssl req -new -key client3new.key -out client3new.csr  -subj "/CN=Client3 new"

#Client Certificate signed by Intermediate CA
openssl x509 -req -in client3new.csr -CA intermediateCA3new.pem -CAkey intermediateCA3new.key -CAcreateserial -sha256  -out client3new.pem -days 90

上記の状態で、過去に作成したクライアント証明書と今回作成したClient3new.pemを利用した接続確認を行ったところ、Client3new.pem以外は接続できました。
では、今回作成した新IntermediateCA3証明書をトラストストアに登録したいと思います。

以下のコマンドで、CA証明書を連結し、トラストストアに反映しました。

awk 1 rootCAUpdated.pem intermediateCA1.pem intermediateCA2Updated.pem intermediateCA3new.pem intermediateCA4Updated.pem > truststore

VersionId=$(aws s3api put-object --bucket mtlstest-truststores3bucket-6rdlcmrisggk --key truststore --body  truststore | jq -r '.VersionId' )

aws apigatewayv2 update-domain-name \
              --domain-name api.xxxx.xxxx\
              --domain-name-configurations CertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/12345678-626f-48c5-b10b-fc51b4545730,EndpointType=REGIONAL \
              --mutual-tls-authentication TruststoreUri=s3://mtlstest-truststores3bucket-6rdlcmrisggk/truststore,TruststoreVersion=$VersionId



上記のコマンドが反映された後に接続確認を以下のコマンドで実施しました。結果はclient3は秘密鍵が更新されたことにより、接続エラーとなり、Client3newは無事接続できました。

curl --key  client1.key --cert client1.pem   https://api.xxxx.xxxx/hoge/auth
curl --key  client2.key --cert client2.pem   https://api.xxxx.xxxx/hoge/auth
curl --key  client3.key --cert client3.pem   https://api.xxxxx.xxxx/hoge/auth
curl --key  client4.key --cert client4.pem   https://api.xxxxx.xxxx/hoge/auth
curl --key  client2new.key --cert client2new.pem   https://api.xxxxx.xxxx/hoge/auth

curl --key  client3new.key --cert client3new.pem   https://api.xxxxxx.xxxxxx/hoge/auth


まとめ

冒頭で、以下のことを確認したいこととしてあげました。結果を書きたいと思います。
1. API Gatewayのトラストストア更新前でも、新規に発行されたCA証明書を利用して生成されたクライアント証明書が接続できるか否か
接続できる。新規に発行された証明書のSubjectが同じで、秘密鍵も変わっていなければ接続できる。

2. CA証明書が更新されても秘密鍵が更新されていなければ以前のCA証明書で生成された、有効期間内のクライアント証明書が継続利用できること
秘密鍵が更新されていなければ、接続可能

3. ついでに確認したいこと(期限切れの証明書をAPI Gatewayには登録できるのか?)`
トラストストアに登録はできた。

4. ついでに確認したいこと(既に登録済みの期限が切れの中間証明書をAPI Gatewayは検証でどう扱うのか?)
クライアント証明書を利用して接続する際にエラーになった。

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

Amazon ECSで、タスクの停止理由などをAWS CLIから確認する

What's?

AWS Fargateを使っていて、タスクの起動に失敗した時にどうやって確認したらいいか覚えられなかったのでメモしようかなと。

AWSマネージメントコンソールで以下の方法で見てもいいのですが、毎回見るのも面倒なので、AWS CLIで見たいのです。

停止したタスクのエラーの確認

環境

利用するAWS CLIのバージョンは、こちら。

$ aws --version
aws-cli/2.0.52 Python/3.7.3 Linux/4.15.0-112-generic exe/x86_64.ubuntu.20

AWSのクレデンシャルは、環境変数で設定しているものとします。

$ export AWS_ACCESS_KEY_ID=...
$ export AWS_SECRET_ACCESS_KEY=...
$ export AWS_DEFAULT_REGION=ap-northeast-1

確認

まずは、確認対象のクラスターを確認します。

list-clusters

$ aws ecs list-clusters

AWS CLIのドキュメント例ですが、こんな結果が得られるので、ECSクラスター名を確認しましょう。

{
    "clusterArns": [
        "arn:aws:ecs:us-west-2:123456789012:cluster/MyECSCluster1",
        "arn:aws:ecs:us-west-2:123456789012:cluster/AnotherECSCluster"
    ]
}

ここでは、MyECSCluster1AnotherECSClusterなどがクラスター名ですね。

次に、タスクを確認します。

list-tasks

$ aws ecs list-tasks --cluster [クラスター名]

# たとえば
$ aws ecs list-tasks --cluster MyECSCluster1

結果の例。

{
    "taskArns": [
        "arn:aws:ecs:us-west-2:123456789012:task/a1b2c3d4-5678-90ab-cdef-11111EXAMPLE",
        "arn:aws:ecs:us-west-2:123456789012:task/a1b2c3d4-5678-90ab-cdef-22222EXAMPLE"
    ]
}

この時、--desired-statusというオプションを指定しない場合、RUNNINGのタスクしか表示されません。

RUNNINGPENDINGSTOPPEDのいずれかを指定することができるので、適切に指定しましょう。

それぞれの意味は、タスクのライフサイクルを参照。

トラブルシュートの場合は、PENDINGSTOPPEDを指定することになります。

$ aws ecs list-tasks --cluster [クラスター名] --desired-status STOPPED

# たとえば
$ aws ecs list-tasks --cluster MyECSCluster1 --desired-status STOPPED

サービスのdesiredCountとの関係で、タスクが多数表示されて困るよという場合は、--max-itemsを指定して表示件数を絞りましょう。

$ aws ecs list-tasks --cluster [クラスター名] --desired-status STOPPED --max-items 3

これでタスクのARNが得られるので、このARNを使用してタスクの状態を確認します。

describe-tasks

$ aws ecs describe-tasks --cluster [クラスター名] --tasks [タスクのARN]

# たとえば
$ aws ecs describe-tasks \
    --cluster MyCluster \
    --tasks arn:aws:ecs:us-west-2:123456789012:task/MyCluster/1234567890123456789

ドキュメントのアウトプット例です。

    "tasks": [
        {
            "taskArn": "arn:aws:ecs:us-west-2:123456789012:task/MyCluster/1234567890123456789",
            "clusterArn": "arn:aws:ecs:us-west-2:123456789012:cluster/MyCluster",
            "taskDefinitionArn": "arn:aws:ecs:us-west-2:123456789012:task-definition/sample-fargate:2",
            "overrides": {
                "containerOverrides": [
                    {
                        "name": "fargate-app"
                    }
                ]
            },
            "lastStatus": "RUNNING",
            "desiredStatus": "RUNNING",
            "cpu": "256",
            "memory": "512",
            "containers": [
                {
                    "containerArn": "arn:aws:ecs:us-west-2:123456789012:container/a1b2c3d4-5678-90ab-cdef-11111EXAMPLE",
                    "taskArn": "arn:aws:ecs:us-west-2:123456789012:task/MyCluster/1234567890123456789",
                    "name": "fargate-app",
                    "lastStatus": "RUNNING",
                    "networkBindings": [],
                    "networkInterfaces": [
                        {
                            "attachmentId": "a1b2c3d4-5678-90ab-cdef-22222EXAMPLE",
                            "privateIpv4Address": "10.0.0.4"
                        }
                    ],
                    "healthStatus": "UNKNOWN",
                    "cpu": "0"
                }
            ],
            "startedBy": "ecs-svc/1234567890123456789",
            "version": 3,
            "connectivity": "CONNECTED",
            "connectivityAt": 1557134016.971,
            "pullStartedAt": 1557134025.379,
            "pullStoppedAt": 1557134033.379,
            "createdAt": 1557134011.644,
            "startedAt": 1557134035.379,
            "group": "service:fargate-service",
            "launchType": "FARGATE",
            "platformVersion": "1.3.0",
            "attachments": [
                {
                    "id": "a1b2c3d4-5678-90ab-cdef-33333EXAMPLE",
                    "type": "ElasticNetworkInterface",
                    "status": "ATTACHED",
                    "details": [
                        {
                            "name": "subnetId",
                            "value": "subnet-12344321"
                        },
                        {
                            "name": "networkInterfaceId",
                            "value": "eni-12344321"
                        },
                        {
                            "name": "macAddress",
                            "value": "0a:90:09:84:f9:14"
                        },
                        {
                            "name": "privateIPv4Address",
                            "value": "10.0.0.4"
                        }
                    ]
                }
            ],
            "healthStatus": "UNKNOWN",
            "tags": []
        }
    ],
    "failures": []
}

停止したタスクには、stoppedReasonなどが追加され、停止した理由を確認することができます。

たとえば、DockerHubからイメージをPullできなかった場合は、こんな感じですね。

            "stopCode": "TaskFailedToStart",
            "stoppedReason": "CannotPullContainerError: failed to resolve ref \"docker.io/library/nginx:1.19.2\": failed to do request: Head https://registry-1.docker.io/v2/library/nginx/manifests/1.19.2: dial tcp 3.218.162.19:443: i/o timeout",
            "stoppingAt": "2020-09-26T17:34:13.490000+09:00",

これで、

停止したタスクのエラーの確認

を見ているのと同じことになります。

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

英語力が無いSEの暫定的な翻訳ツール活用術(2020年9月時点)

■記載の理由

私はインフラエンジニアとして活動して来ましたが、これまでは英語力が無くてもなんとか業務を実施することができていました。理由として日本語のドキュメントが提供されているサービスが多く、英語が読めなくてもなんとかなっていたからです。しかしながら、最近になってAWSやAzureについて学習しようとすると、多くの有益なドキュメントが英語で発行されており、英語が出来ないことが大きなデメリットとなってきました。

最も衝撃的だったのがAWSの資格試験です。
AWSの資格試験は日本語でも受験できるのですが、誤訳が多々あり、その結果英語と時と答えが変わってしまうものもあります。しかし「英語とに日本語の問題文で答えが異なった場合、英語の方が正となります。」が前提となっています。ようするに、英語で問題文を読めない限り、誤訳のせいでつまずくリスクがある訳です・・・・。結論として、

$\huge{『英語はSEに必要なスキルなので勉強しましょう!』}$

になります。当たり前ですが。
しかしながら、直ぐには英語力を伸ばすことは難しいです。資格は『今』取得する必要があって、英語力が伸びきるまで待ってくれないからです。その為、暫定的に翻訳ツールに頼ることになるのですが、今回はマイクロソフト資格試験を受けるにあたって、かなり苦労したので、その翻訳ツールの活用方法を整理しておこうと思いました。もし、一読頂けた方が知らない方法があれば、活用してもらえればと思います。

■読んでほしい人

・英語が苦手だが、英語のドキュメントを読む必要性が発生している人。

■翻訳場面①:英語のサイトを翻訳する

私の場合、マイクロソフト社の「MS-900: Microsoft 365 Fundamentals」とう資格試験にチャレンジした際に、ラーニングサイトの半分くらいが日本語版がリリースされておらず英語サイトという問題にぶちあたりました。下記のような形です。
image.png

このような場合に利用するのがWebブラウザの翻訳機能になります。

第一候補:Google chrome

私のお勧めは『Google chrome』となります。
image.png
設定画面から「言語」を選択して「母国語以外のページで翻訳ツールを表示する」のチェックを入れることで、英語サイトを開いた時に翻訳確認を実施するようになります。
image.png
使い方は非常に簡単で、英語のサイトを開くとURLバーの右端に翻訳ボタンが表示されます。
image.png
ボタンを押すと上記のウィンドウが表示される為、「日本語」を選択すれば、表示サイトが日本語に翻訳されます。また、「英語を常に翻訳」のチェックを入れていれば、英語のサイトが表示された場合、問答無用で日本語に翻訳してくれるようになります。

翻訳力も抜群ですので、安心して利用できます。

第二候補:Microsoft Edge

第二候補は『Microsoft Edge』です。
image.png
設定画面から「言語」を選択して「自分が読み取ることができない言語のページの翻訳を提案する」のチェックを入れることで、英語サイトを開いた時に翻訳確認を実施するようになります。
image.png
使い方は非常に簡単で、英語のサイトを開くとURLバーの右端に翻訳ボタンが表示されます。
image.png
ボタンを押すと上記のウィンドウが表示される為、「翻訳」を選択すれば、表示サイトが日本語に翻訳されます。また、「英語のページを常に翻訳する」のチェックを入れていれば、英語のサイトが表示された場合、問答無用で日本語に翻訳してくれるようになります。

この『Microsoft Edge』は、マイクロソフトが今年度になって従来の『旧Edge』を捨ててChromium版の『新Edge』を発表し、現在はほとんどの人のEdgeが『新Edge』になっているかと思います。じゃあ翻訳エンジンも『Chrome』と同じになるかというと、『Translator for Microsoft Edge』というマイクロソフト社のアドオンを利用しているようなので、差別化はされています。

ブラウザ翻訳機能の利用方法

私は翻訳機能を利用して学習する際には、2つのブラウザを利用して同じページを表示して学習しています。基本的に『Google chrome』に翻訳画面を表示されます。そして『Microsoft Edge』で英語の原文を表示されます。そして、基本的に翻訳画面を見つつ、怪しいところは原文を確認しながら進めていく感じです。

例えば以下のように翻訳した際に、製品名等を日本語に訳してしまう場合があります。「Exchange」(ソフトウェア名)を「両替」と訳してしまったり、「SharePoint」(ソフトウェア名)を「共有ポイント」と訳してしまったり、本来訳さないでほしい英文のままでよい部分まで訳されてしまう訳です。そうなると意味が解らなくなります。こういった問題を回避するためにも、ブラウザを2つ利用した運用を推奨します。

原文 Chrome翻訳 Edge翻訳
image.png image.png image.png

■翻訳場面②:英語のサイトを翻訳する

翻訳場面①ではサイト自体の翻訳でしたが、一部の文書のみ翻訳したい場合もあります。その際に利用しているのは『Google 翻訳』となります。普通にGoogleのWebサイトの翻訳機能です。やはり、Googleさんの翻訳機能はすばらしいです。頼らざるを得ません。
image.png

■翻訳場面③:英語のドキュメントを翻訳する

次は英語のドキュメントの翻訳方法です。

第一候補:Google 翻訳

第一候補は『Google 翻訳』です。
image.png
上記のPDFファイルを翻訳するとします。
image.png
Google翻訳画面で「ドキュメント」を選択し、「パソコンを参照」を選択します。
image.png
ファイルを選択して上記画面に遷移したら「翻訳」を選択します。
image.png
翻訳後の画面が表示されます。
image.png
翻訳後の文書にマウスカーソルを添えると原文が表示されますので、比較しながら確認できます。
image.png

Webページで印刷を選択しプリンターで「PDFとして保存」を選択することで、PDFで保存が可能です。翻訳が非常に早いので便利です。

第二候補:DocTranslator

私も長らく愛用してきた『Google 翻訳』ですが、弱点もあります。図が入っている資料はうまく変換できないことです。

image.png

例えば、Microsoftの上記資料を『Google 翻訳』で翻訳します。
image.png

残念ながら図が崩壊してしまいます。これでは何が書いているのか理解できません。

そこで『DocTranslator』です。「powered by Google Translate」と記載があり、Googleの翻訳エンジンを利用しているので、基本的に翻訳力は変わりません。翻訳にも時間がかかりますが、図のレイアウトを維持したまま翻訳を実施してくれます。
image.png
まず、「ファイルをダウンロード」を選択します。アップロードするのですが、なぜかボタンはダウンロードとなっています。
image.png
翻訳したいファイルをアップロードすると上記画面が表示される為、「翻訳する」を選択します。翻訳が開始され、翻訳済みのPDFファイルがダウンロードフォルダに格納されます。
image.png
図のレイアウトを残した状態で翻訳が実施されています。

ドキュメント翻訳機能の活用方法

2つの翻訳機能を紹介しましたが、図が無い場合は翻訳が早い『Google 翻訳』を、図がある場合は図のレイアウトを残せる『DocTranslator』を使う形で、使い分けするのがよいかと思います。

■まとめ

英語力が無い場合は、いかに効率よく翻訳して内容を理解するかに心血を注ぐ必要があります。そういったSEの方々が英語力を身に付けるまでの暫定対策として、上記の方法が活用いただければ幸いです。ただ、最終的には、
$\huge{『英語はSEに必要なスキルなので勉強しましょう!』}$

ということは、私も痛感するところです。

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

AWS SAA-C02版合格体験記(2020/09)

これは何か

自分がAWS SAA(ソリューションアーキテクト-アソシエイト)を合格するまでにやってきたことを書いていきます。

aws-certified-solutions-architect-associate.png

1. 勉強前の自分について

1.1. エンジニア経験

正直、どこにでもいるような経歴かなとは思っております。エンジニア経験としてはお世辞にも経験豊富とは言えませんね...
- システムエンジニア2年
- データサイエンティスト1年半
- 基本情報技術者試験合格

1.2. AWSの利用経験

下記経験を見れば分かる通り、少し齧ったことのある程度です。
- EC2, ELB, VPCなどを用いた簡単なアプリケーションの開発経験
- S3やEMRを利用したデータ分析経験

1.3. 勉強時間

08/11〜09/23の44日間、毎日約2時間ほど勉強していましたので、勉強時間としては大体80~100時間程度かなと思います。

2. 勉強したこと

2.1. AWS認定アソシエイト3資格対策本

勉強開始当初は9月中に3資格一気に取ってしまおうと思い、この本を買いました(後々、模擬試験を解いた時に甘かったなと思うのですが)。この本の良かったところは、チュートリアルが紹介されているところです。試験に合格するだけならAWSに一度も触らずとも合格できてしまうのですが、それでは本質的ではない気がします。この本のおかげで否が応でも「実際に触って試してみなきゃ!」という気持ちになりました。ただ上述した通り、SAAに合格するためだけなら、この本はやらなくても良かったかもと思っております。

2.2. Udemy AWS SAA 模擬試験問題集6回分

Udemyで6回分の模擬試験が3600円で受けられる大変お得?な問題集。解説も充実している。後述するが公式の模擬試験は相当割高な上、学習にはほとんど役に立たない。また対策本に模擬試験が付いているものもあるが、どれも1回分のものばかりだったのでSAAの問題をたくさん解きたいという方におすすめ。しかし、問題が全体的に難しい。正直、この問題解けなくても合格できるだろうくらいの問題ばかり出題されている印象を受けた。もちろん、試験に出ることもあるし、問題によっては頻出事項の内容も出題されるのでやっておいて損はない。合格できる自信がない人や時間に余裕がある人向けな気がする。
ちなみに自分は6回とも正答率40~50%程度。解説などを読んで2回目は90~100%の正答率になった。また、試験前に6回分全て解き直しているので計3周したことになる(3周目は時間が空いていることもあり、正答率は80~90%になってしまった)。

2.3. この1冊で合格! AWS認定ソリューションアーキテクト - アソシエイト テキスト&問題集

Udemyの模擬試験を解き終えた後、よく出題されるサービスがどれなのか分かってきたタイミングで再度、体系的に勉強し直すことにした。本書は会社の友人が勧めてくれたものであり、評判が良かったので購入。非常に良かった。頻出ポイントに絞ってテキストが構成されており、内容も分かりやすい。頻出ポイントもUdemyで散々間違えた箇所だったので、すんなり頭に入ってきて整理もできた。SAAに合格するためだけなら、確かにこの本1冊で充分な気がする。ちなみに本書の最後に模擬試験が付いており、そちらの正答率は70%程度であった(Udemyの模擬試験のように難易度の高いものはなく、本番により近い模擬試験のような気がする)。

2.4. AWS公式の模擬試験

公式が出しているオンライン模擬試験であり、いつでも受験が可能。しかし、1回2000円かかる割に問題は20問(本番は65問)、解答解説なし、試験結果は4分野(コスト最適化やセキュリティといった項目)での正答率を出してくれるだけという非常に割高なもの。正直、これは一番やらなくても良かったんじゃないかと思っている。どうしても合格できるか不安でしょうがない人向けに感じた。ちなみに正答率は70%程度。2.3.で受けた模擬試験と難易度的にはほぼ同じだったんじゃないかと感じている。

2.5. 試験直前の復習

Udemyや書籍などで模擬試験を解いていくとともに、間違えた問題の中で抑えておくべきポイントをノートにサービス別でまとめていた。それを試験直前は眺めて本番に臨んだ。

3. 試験結果

3.1. スコア

100~1000点の試験で787点を取り、無事合格(合格最低点は720点)。大体7割強のスコアだし、粗方想定通りの結果だった。分野別での評価は下記の通り。おそらく全体的に得点できていたと思われる。
スクリーンショット 2020-09-26 16.13.03.png

3.2. もし不合格だった場合

試験直前に見直す用のノートを見返すと、明らかに記述量の多いサービスがいくつかある(EC2, VPC, S3, RDSなど)。もし不合格で再受験に向けて勉強し直すことになった場合は、ノートを元に優先順位を付けながらBlack Beltを読み込もうと考えていた。本来、Black Beltを読み込んでから試験に臨むべきな気はしていたが、9月中に合格したかったこと、早くSAA取ってAWS上でアプリ公開したり機械学習モデルの分析環境整備してみたいなと思っていたので、受かると思ったタイミングで受けてしまった。

3.3. 受けてみての感想

AWSについては少し齧ったことがあるものの苦手意識があった。今回、SAAで各サービスの位置付けなどが理解できたのが非常に良かった。また、チュートリアルなどで触れる機会もたくさんあったので、実業務で触れる際も臆することなく取り組めそうである(事実、今取り組んでいるものもある)。今後は、GCP周りのサービスも勉強していきたいと感じている。
ちなみに試験難易度はITパスポート程度と聞いてましたが、基本情報技術者程度の難しさではあったんじゃないかと感じました(ITパスポート受けたことありませんが)。皆さんはどうでしょうか??

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

【AWS CloudFormation】EC2のユーザーデータでWebサーバーをインストールする

はじめに

EC2の構築作業を省力化のため、CloudFormationにおいてEC2のユーザーデータ(EC2起動時に実行されるシェルスクリプト)を使って、Webサーバーをインストールする方法を学習した。その方法を備忘録として残したい。

手順

CloudFormation>スタック>スタックの作成で、ステップ1では下記のように設定する。
yamlのテンプレートは以下を指定する。
設定し終えたら、次へ進む。

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

テンプレート↓
ImageId: ami-〇〇については、〇〇の部分をEC2でAMI IDにあるものに直して入力する。

---
Parameters:
  SSHKey:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Name of an existing EC2 key pair for SSH access to the EC2 instance

Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      AvailabilityZone: ap-northeast-1a
      ImageId: ami-〇〇
      InstanceType: t2.micro
      KeyName: !Ref SSHKey
      SecurityGroups:
        - !Ref SSHSecurityGroup
      # ユーザーデータからWebサーバーをインストールする
      UserData: 
        Fn::Base64: |
          #!/bin/bash -xe
          yum update -y
          yum install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "Hello World from user data" > /var/www/html/index.html

  # EC2セキュリティグループ
  SSHSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SSH and HTTP
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      - CidrIp: 0.0.0.0/0
        FromPort: 80
        IpProtocol: tcp
        ToPort: 80

ステップ2でスタックの名前とSSHKeyを指定する。今回は「user-data-example」とした。SSHKeyではすでに作成されたSSHKeyを適宜指定する。
次へ進む。
スクリーンショット 2020-09-25 16.45.37.png

ステップ3では特に何も入力せず、デフォルトのままで次へ進む。
スクリーンショット 2020-09-26 14.36.01.png
スクリーンショット 2020-09-25 16.46.09.png
ステップ4で内容に誤りがない確認し、スタックの作成を押す。
スクリーンショット 2020-09-25 16.47.23.png
スクリーンショット 2020-09-25 16.47.30.png
該当スタックが正しく作成されているか、ステータスで確認する。
スクリーンショット 2020-09-26 14.38.22.png
EC2>インスタンスに行き、作成されたインスタンスの詳細を確認する。
問題がなければパブリックIPv4アドレスをURLに入力すると、「Hello World from user data」が画面に表示される。
スクリーンショット 2020-09-26 14.41.17.png

参考
Ultimate AWS Certified SysOps Administrator Associate 2020
https://dev.classmethod.jp/articles/using-variables-in-ec2-user-data-in-cloudformation/

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

[AWS CLI v2]バージョンチェックとアップグレードをラクにしよう

はじめに

何事も最新化しておくことは重要ですが 「XXX(Software) アップグレード」とググって手順を1つずつ実施...は手間ですよね...
今年(2020年)GAされた AWS CLI v2の最新バージョンチェックとアップグレードを楽に行う方法 を見つけたので本記事でシェアします。改造版も掲載していますので良ければご利用ください。

前提

当たり前かもしれませんが AWS CLI v2 がインストールされていること です。

導入

jamesls/alias のシェルを ~/.aws/cli の階層にaliasという名称で作成するだけです。(再起動は不要)

主な用途は

  • AWS CLI v2のバージョンチェック
  • AWS CLI v2のアップグレード

になります。
それぞれ以下2つの用法をご確認ください。

用法1:AWS CLI v2 の バージョンチェックを行う

下記コマンドを実施することで 最新バージョンと手元のcliのバージョンが一致しているか確認 できます。

shell
aws check-upgrade

用法2:AWS CLI v2 の アップグレードを行う

下記コマンドを実施することで 最新バージョンにアップグレードする ことができます。
なお、既に最新版が導入されている場合も最新版インストールの処理が実行される仕様になっています。

shell
aws upgrade

改修版:最新バージョンでなかった場合、アップグレードする

個人的には 「check-upgradeでバージョンが異なったらupgradeしてほしいな」 と思ったので
Gistに手を加えてみました
是非ご利用ください。

おわりに

非常に簡素な記事で恐縮ですが、同様の記事がなかったためシェアすることを目的に記載しました。
ちなみに awslabsのaliasも非常に有用 ですので、ご利用を検討することをオススメします。

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

初心者がAWSを始めてみた(1)

はじめに

最近、仕事でAWSに触れる機会が増えてきたので、
個人的に勉強がてら始めてみようと思いました。

ここでは、初心者目線でAWSに触れていく様子を書いていこうと思います。

AWSとは?

AWSとは「Amazon Web Service」の略で、
かの有名なAmazon社が提供しているクラウドサービスのことです。

ネットワークを経由して仮想コンピューターやストレージをはじめとする様々なサービスが提供されています。

詳しいことは、またどこかのタイミングで投稿したいと思いますw

とりあえずはじめよう!

AWSは様々なサービスを提供していますが、
全部を理解するのは難しいです。
そもそも触りながらの方が覚えやすいと個人的に思ってます。(「習うより慣れろ」っていうんですかねw)

AWSを利用するにあたりアカウントを作成する必要があります。
※アカウントの登録自体は無料ですが、サービスの利用にお金が発生することがあります。
※アカウント登録から1年間はサービス利用料が無料の枠もあります。(https://aws.amazon.com/jp/s/dm/landing-page/create-free-account/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc)

早速アカウントを作成してみましょう。

AWSアカウント作成

ステップ1
まずは、AWS公式サイトから「今すぐ無料サインアップ」を押します。(https://aws.amazon.com/jp/)
スクリーンショット 2020-09-26 12.46.05.png

ステップ2
必要事項を入力します。入力し終えたら「続行」を押します。
スクリーンショット 2020-09-26 12.49.58.png

ステップ3
必要事項を入力します。入力し終えたら「アカウントを作成して続行」を押します。
※氏名や住所は半角英数字での入力が指定されています。
スクリーンショット 2020-09-26 13.00.44.png

ステップ4
支払情報(クレジットカード情報)を入力します。入力したら次の画面に移動します。
スクリーンショット 2020-09-26 13.07.19.png

ステップ5
本人確認をします。必要な情報を入力して本人確認をしてください。
スクリーンショット 2020-09-26 13.13.57.png

ステップ6
プランを選択します。お金を描けないのであれば「ベーシックプラン」一択です。
スクリーンショット 2020-09-26 13.18.37.png

お疲れ様でした。
これでAWSアカウントを作成できたので「コンソールにサインイン」を押して、
AWSマネジメントコンソールにログインしましょう。

ログイン画面が表示されるので、ルートユーザーにチェックを入れて、
先ほど設定したアカウント情報でログインします。
スクリーンショット 2020-09-26 13.28.32.png

ログインが成功するとAWSマネジメントコンソールが表示されます。
スクリーンショット 2020-09-26 13.32.01.png

次回

今回はAWSアカウントを作成するところまでで終えたいと思います。
AWSマネジメントコンソールからAWSのサービスを操作できますが、
始めるまであともう少しだけ準備が必要です。

今回作成したアカウントは「ルートユーザー」というものです。
簡単に言ってしまえば「なんでもできる神様アカウント」です。
このアカウント情報が漏洩するととんでもないことが起きてしまうのはお分かりですね?w

そこで、次回はアカウント情報を守る設定とIAMユーザーについて投稿します。
是非読んでくださいね〜

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

AWS Systems Manager、Lambdaを使ってS3のファイルをEC2(Windows)内にダウンロード(同期)してみた。

概要図

Lambda ⇒ Systems Manager ⇒ EC2 ⇒(同期コマンド)⇒ S3

▼参考(にさせていただきました!)
https://dev.classmethod.jp/articles/lambda-s3-ec2-windows-sync/

作業ステップ

▼前提
・S3にバケットを作成し、ファイルをアップロード済み

①IAMロールを作成(EC2用)

AmazonEC2RoleforSSM
※上記権限は範囲が広いため、別のポリシーへの移行が推奨されていますので、
 仕事で使用する場合は下記ページをご確認いただければと思います。
 https://dev.classmethod.jp/articles/check-amazonec2roleforssm-policy/

②EC2インスタンス作成(windowsサーバ ※ami-038c59e921e8b9c7d 使いました)

ロールに”①”をアタッチ
⇒SystemsManagerでEC2のオペレーションができるようになる
※SSMエージェントはデフォルトでインストールされていました。

③IAMユーザを作成(EC2用)

AmazonS3FullAccess(S3フルアクセス)

④EC2にRDP(リモートデスクトップ)し、AWS CLIをインストール

私はMSIインストーラでインストールしました。
https://dev.classmethod.jp/articles/first-login-to-ec2-windows/
https://docs.aws.amazon.com/cli/latest/userguide/install-windows.html

⑤EC2にRDP(リモートデスクトップ)し、AWS CLIにアクセスキー・シークレットアクセスキーを設定

”③”で振り出した、アクセスキーシークレットアクセスキーを設定
⇒これにより、AWS CLIで、S3にアクセスできるようになる。

⑥IAMロールを作成(lambda用)

AmazonEC2FullAccess
AmazonSSMFullAccess
※lambdaデフォルトポリシーに上記を追加

⑦lambdaを作成

EC2にS3のファイルを同期するようなコマンドを実行させる。

結果

S3のファイルがEC2に同期されました!
キャプチャ.JPG

はまったポイント

①lambdaからaws cliのコマンドを実行すると、下記のエラーが表示される。

aws : The term 'aws' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a path
was included, verify that the path is correct and try again.

まぁ、はまりました(笑)
▼試したこと
①コマンドを管理権限で実行する方法を探す
 ⇒sudo的なのはない、ただファイルを置けばできそうな感じ、、いったん放置
②EC2にadmin権限なしのユーザを作ってログインし、Power Shellでaws cliコマンド実行
 ⇒普通に実行できる。(admin権限周りは関係なさそう)
③EC2にデフォルトでインストールされているSSMエージェントのverを最新にしてみた
 AWSコンソール>AWS System Manager>マネージドインスタンス>Agent auto updateを押下
 ⇒ダメもとでためしただけなんですが、、解決!!!

まさか、これなん、という感じでした。
いやー、無事解決、気持ちよかったです(*'▽')

なるほどポイント

元々、パブリックサブネットでEC2/VPC外lambdaで実行していました。
EC2をプライベートサブネット(NATつき)に移動させても問題ないのか、確認してみました。
⇒Lamdbaからのコマンドが実行されなくなりました。

試していないですが、下記の作業で可能となるようです。
VPCエンドポイントは双方向なんですね。
https://dev.classmethod.jp/articles/tsnote-private-ec2-ssm-vpc-endpoint/

以上です。勉強になりました。

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

AWS CDK テンプレート集

はじめに

AWS CDKを使ってバックエンドテンプレートを作ったので、備忘を兼ねていくつか紹介をしようと思います。
コードが綺麗じゃないのはご愛嬌で。。。

目次

  • とりあえずAPIを作りたい人用(API Gateway + Lambda)
  • プロトタイプを作成したい人用
  • 認証機能付きでアプリを作成したい人用

1. とりあえずAWS上でAPIを作りたい人用(API Gateway + Lambda)

バックエンド構成図

cdk-create-api.png

ソースコード

以下のGitHubのリポジトリに保管してあります。
READMEに従ってデプロイをすれば上のようなリソースがAWS上で立ち上がります。

>>https://github.com/NagataRyuji0630/aws-cdk-create-api

stackファイルの内容だけ見たいという人のために、以下にコードを記載しておきます。
aws-cdk-create-api-stack.ts
import * as cdk from '@aws-cdk/core';
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';
import { RestApi, LambdaIntegration, IResource, MockIntegration, PassthroughBehavior } from "@aws-cdk/aws-apigateway";
import { RetentionDays } from '@aws-cdk/aws-logs';

//**************************************************** */
// 変数部分は自由に編集してください。
const yourFunctionName = 'your-function';
const restApiName = 'your-first-api';
//**************************************************** */

export class AwsCdkCreateApiStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //**************************************************** */
    //LambdaFunctionの作成
    //**************************************************** */
    const yourFunction: Function = new Function(this, 'your-function-id', {
      functionName: yourFunctionName,
      runtime: Runtime.NODEJS_12_X,
      code: AssetCode.fromAsset('src/lambda'),
      handler: 'yourFunction.handler',
      timeout: cdk.Duration.seconds(10),
      environment: {
        TZ: "Asia/Tokyo",
        CORS_URL: "*" // 作成したCloudFrontのエンドポイントを指定する
      },
      logRetention: RetentionDays.TWO_MONTHS,
    });

    //**************************************************** */
    // API Gateway(リソース, メソッド)の作成
    //**************************************************** */
    const api = new RestApi(this, "your-first-api-id", {
      restApiName: restApiName,
      cloudWatchRole: true,

    });
    const scanMeeting = api.root.addResource("your-du");

    const scanMeetingLambdaIntegration = new LambdaIntegration(yourFunction);
    scanMeeting.addMethod("POST", scanMeetingLambdaIntegration);
    addCorsOptions(scanMeeting);
  }
}

//**************************************************** */
// API GatewayのメソッドにOPTIONを追加
//**************************************************** */
export function addCorsOptions(apiResource: IResource) {
  apiResource.addMethod(
    "OPTIONS",
    new MockIntegration({
      integrationResponses: [
        {
          statusCode: "200",
          responseParameters: {
            "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
            "method.response.header.Access-Control-Allow-Origin": "'*'",
            "method.response.header.Access-Control-Allow-Credentials": "'false'",
            "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE'",
          },
        },
      ],
      passthroughBehavior: PassthroughBehavior.NEVER,
      requestTemplates: {
        "application/json": '{"statusCode": 200}',
      },
    }),
    {
      methodResponses: [
        {
          statusCode: "200",
          responseParameters: {
            "method.response.header.Access-Control-Allow-Headers": true,
            "method.response.header.Access-Control-Allow-Methods": true,
            "method.response.header.Access-Control-Allow-Credentials": true,
            "method.response.header.Access-Control-Allow-Origin": true,
          },
        },
      ],
    }
  );
}

2. プロトタイプを作成したい人用

何か作りたいアプリや機能があって、手っ取り早くプロトタイプを作りたいという人用のテンプレートです。

バックエンド構成図

cdk-startup-edition.png

ソースコード

以下のGitHubのリポジトリに保管してあります。
READMEに従ってデプロイをすれば上のようなリソースがAWS上で立ち上がります。

>>https://github.com/NagataRyuji0630/aws-cdk-startup-edition

stackファイルの内容だけ見たいという人のために、以下にコードを記載しておきます。
cdk-templete-startup-edition-stack.ts
import * as cdk from '@aws-cdk/core';
import { Table, AttributeType } from "@aws-cdk/aws-dynamodb";
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';
import { RestApi, LambdaIntegration, IResource, MockIntegration, PassthroughBehavior } from "@aws-cdk/aws-apigateway";
import { RetentionDays } from '@aws-cdk/aws-logs';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';

//**************************************************** */
// 変数部分は自由に編集してください。
const stage = "dev"; // "stg","prd"
const bucketName = 'your-web-dev-bucket'
const projectName = 'yourProject-' + stage; // ステージごとにリポジトリを作り分け可能
const repositoryName = 'your-cdk-repository' + stage;
const branch = 'master'; // 'release','master'; 
const pipelineName = 'yourPipeline-' + stage;
const tableName = "YOUR_TABLE";
const yourFunctionName = 'your-function';
const restApiName = 'your-first-api';
//**************************************************** */

export class CdkTempleteStartupEditionStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //**************************************************** */
    // S3バケットの作成
    //**************************************************** */

    const s3Bucket = new s3.Bucket(this, 's3-bucket-id', {
      bucketName: bucketName, // バケット名を定義
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })

    // Create OriginAccessIdentity
    const oai = new cloudfront.OriginAccessIdentity(this, "my-oai");

    // Create Policy and attach to mybucket
    const myBucketPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["s3:GetObject"],
      principals: [
        new iam.CanonicalUserPrincipal(
          oai.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [s3Bucket.bucketArn + "/*"],
    });
    s3Bucket.addToResourcePolicy(myBucketPolicy);

    //**************************************************** */
    // CloudFrontの定義
    //**************************************************** */

    // Create CloudFront WebDistribution
    new cloudfront.CloudFrontWebDistribution(this, "WebsiteDistribution", {
      viewerCertificate: {
        aliases: [],
        props: {
          cloudFrontDefaultCertificate: true,
        },
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: s3Bucket,
            originAccessIdentity: oai,
          },
          behaviors: [
            {
              isDefaultBehavior: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(365),
              defaultTtl: cdk.Duration.days(1),
              pathPattern: "/*", //ルート直下のファイルを全て参照
            },
          ],
        },
      ],
      errorConfigurations: [
        {
          errorCode: 403,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
        {
          errorCode: 404,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
      ],
    });

    //**************************************************** */
    // ビルドプロジェクトの作成
    //**************************************************** */
    const project = new codebuild.PipelineProject(this, 'project', {
      projectName: projectName, 
      description: 'some description',
      environment: {
        // 環境変数をbuildspec.ymlに設定
        environmentVariables: {
          S3_BUCKET_ARN: {
            type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            value: s3Bucket.bucketArn,
          }
        },
      }
    });

    // S3へ資源反映するために、S3FullAccessRoleをcodeBuildへ付与
    project.addToRolePolicy(new iam.PolicyStatement({
      resources: [s3Bucket.bucketArn, s3Bucket.bucketArn + '/*'],
      actions: ['s3:*']
    }
    ));

    // パイプラインの生成
    const sourceOutput = new codepipeline.Artifact();
    //**************************************************** */
    // ソースアクションの作成
    //**************************************************** */

    // CodeCommitリポジトリの作成
    const repo = new codecommit.Repository(this, 'Repository', {
      repositoryName: repositoryName,
      description: 'Some description.', // optional property
    });

    const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
      actionName: 'CodeCommit',
      repository: repo,
      branch: branch,
      output: sourceOutput,
    });

    //**************************************************** */
    // ビルドアクションの作成
    //**************************************************** */
    const buildAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild',
      project,
      input: sourceOutput,
      outputs: [new codepipeline.Artifact()]
    });

    //**************************************************** */
    // パイプラインの作成
    //**************************************************** */
    new codepipeline.Pipeline(this, 'pipeline', {
      pipelineName: pipelineName,
      stages: [
        {
          stageName: 'Source',
          actions: [
            sourceAction
          ],
        },
        {
          stageName: 'Build',
          actions: [
            buildAction
          ],
        }
      ]
    })

    //**************************************************** */
    // DyanmoDBの作成
    //**************************************************** */
    const table: Table = new Table(this, "your-table-id", {
      partitionKey: {
        name: "id",
        type: AttributeType.NUMBER
      },
      sortKey: {
        name: "password",
        type: AttributeType.STRING
      },
      readCapacity: 1,
      writeCapacity: 1,
      tableName: tableName,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    //**************************************************** */
    //LambdaFunctionの作成
    //**************************************************** */
    const yourFunction: Function = new Function(this, 'your-function-id', {
      functionName: yourFunctionName,
      runtime: Runtime.NODEJS_12_X,
      code: AssetCode.fromAsset('src/lambda'),
      handler: 'yourFunction.handler',
      timeout: cdk.Duration.seconds(10),
      environment: {
        TZ: "Asia/Tokyo",
        TABLE_NAME: table.tableName,
        CORS_URL: "*" // 作成したCloudFrontのエンドポイントを指定する
      },
      logRetention: RetentionDays.TWO_MONTHS,
    });

    table.grantFullAccess(yourFunction);

    //**************************************************** */
    // API Gateway(リソース, メソッド)の作成
    //**************************************************** */
    const api = new RestApi(this, "your-first-api-id", {
      restApiName: restApiName,
      cloudWatchRole: true,

    });
    const scanMeeting = api.root.addResource("your-du");

    const scanMeetingLambdaIntegration = new LambdaIntegration(yourFunction);
    scanMeeting.addMethod("POST", scanMeetingLambdaIntegration);
    addCorsOptions(scanMeeting);
  }
}

//**************************************************** */
// API GatewayのメソッドにOPTIONを追加
//**************************************************** */
export function addCorsOptions(apiResource: IResource) {
  apiResource.addMethod(
    "OPTIONS",
    new MockIntegration({
      integrationResponses: [
        {
          statusCode: "200",
          responseParameters: {
            "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
            "method.response.header.Access-Control-Allow-Origin": "'*'",
            "method.response.header.Access-Control-Allow-Credentials": "'false'",
            "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE'",
          },
        },
      ],
      passthroughBehavior: PassthroughBehavior.NEVER,
      requestTemplates: {
        "application/json": '{"statusCode": 200}',
      },
    }),
    {
      methodResponses: [
        {
          statusCode: "200",
          responseParameters: {
            "method.response.header.Access-Control-Allow-Headers": true,
            "method.response.header.Access-Control-Allow-Methods": true,
            "method.response.header.Access-Control-Allow-Credentials": true,
            "method.response.header.Access-Control-Allow-Origin": true,
          },
        },
      ],
    }
  );
}

3. 認証機能付きでアプリを作成したい人用

2のバージョンの焼き増しですが、上に加えて、アプリ作成時に認証機能も実装したいという人用のテンプレートです。

バックエンド構成図

cdk-additional-edition.png

ソースコード

以下のGitHubのリポジトリに保管してあります。
READMEに従ってデプロイをすれば上のようなリソースがAWS上で立ち上がります。
※Lambdaの設定は作者が趣味で作っていたアプリのソースの一部を使ってます。利用する際はLambdaの設定とsrc/lambda直下のファイルを編集または新規作成してください。

>>https://github.com/NagataRyuji0630/aws-cdk-authentication-edition

stackファイルの内容だけ見たいという人はコチラ。
cdk-templete-additional-edition-stack.ts
import * as cdk from '@aws-cdk/core';
import { Table, AttributeType } from "@aws-cdk/aws-dynamodb";
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';
import { RestApi, LambdaIntegration, IResource, MockIntegration, PassthroughBehavior } from "@aws-cdk/aws-apigateway";
import { RetentionDays } from '@aws-cdk/aws-logs';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as cognito from '@aws-cdk/aws-cognito';

//**************************************************** */
// 変数部分は自由に編集してください。
const stage = "dev"; // "stg","prd"
const bucketName = 'your-web-dev-bucket'
const projectName = 'myProject-' + stage; // ステージごとにリポジトリを作り分け可能
const repositoryName = 'my-cdk-repository' + stage;
const branch = 'master'; // 'release','master'; 
const pipelineName = 'myPipeline-' + stage;
const tableName = "MY_TABLE";
const restApiName = 'my-first-api';
//**************************************************** */

export class CdkTrainingStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //**************************************************** */
    // S3バケットの作成
    //**************************************************** */

    const s3Bucket = new s3.Bucket(this, 's3-bucket-id', {
      bucketName: bucketName, // バケット名を定義
      websiteIndexDocument: 'test.html',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })

    // Create OriginAccessIdentity
    const oai = new cloudfront.OriginAccessIdentity(this, "my-oai");

    // Create Policy and attach to mybucket
    const myBucketPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["s3:GetObject"],
      principals: [
        new iam.CanonicalUserPrincipal(
          oai.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [s3Bucket.bucketArn + "/*"],
    });
    s3Bucket.addToResourcePolicy(myBucketPolicy);

    //**************************************************** */
    // CloudFrontの定義
    //**************************************************** */

    // Create CloudFront WebDistribution
    new cloudfront.CloudFrontWebDistribution(this, "WebsiteDistribution", {
      viewerCertificate: {
        aliases: [],
        props: {
          cloudFrontDefaultCertificate: true,
        },
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: s3Bucket,
            originAccessIdentity: oai,
          },
          behaviors: [
            {
              isDefaultBehavior: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(365),
              defaultTtl: cdk.Duration.days(1),
              pathPattern: "/*", //ルート直下のファイルを全て参照
            },
          ],
        },
      ],
      errorConfigurations: [
        {
          errorCode: 403,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
        {
          errorCode: 404,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
      ],
    });

    //**************************************************** */
    // ビルドプロジェクトの作成
    //**************************************************** */
    const project = new codebuild.PipelineProject(this, 'project', {
      projectName: projectName,  // ビルドプロジェクトを定義
      description: 'some description',
      environment: {
        // 環境変数をbuildspec.ymlに設定
        environmentVariables: {
          S3_BUCKET_ARN: {
            type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            value: s3Bucket.bucketArn,
          }
        },
      }
    });

    // S3へ資源反映するために、S3FullAccessRoleをcodeBuildへ付与
    project.addToRolePolicy(new iam.PolicyStatement({
      resources: [s3Bucket.bucketArn, s3Bucket.bucketArn + '/*'],
      actions: ['s3:*']
    }
    ));

    // パイプラインの生成
    const sourceOutput = new codepipeline.Artifact();
    //**************************************************** */
    // ソースアクションの作成
    //**************************************************** */

    // CodeCommitリポジトリの作成
    const repo = new codecommit.Repository(this, 'Repository', {
      repositoryName: repositoryName,
      description: 'Some description.', // optional property
    });

    const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
      actionName: 'CodeCommit',
      repository: repo,
      branch: branch,
      output: sourceOutput,
    });

    //**************************************************** */
    // ビルドアクションの作成
    //**************************************************** */
    const buildAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild',
      project,
      input: sourceOutput,
      outputs: [new codepipeline.Artifact()]
    });

    //**************************************************** */
    // パイプラインの作成
    //**************************************************** */
    new codepipeline.Pipeline(this, 'pipeline', {
      pipelineName: pipelineName,
      stages: [
        {
          stageName: 'Source',
          actions: [
            sourceAction
          ],
        },
        {
          stageName: 'Build',
          actions: [
            buildAction
          ],
        }
      ]
    })

    //**************************************************** */
    // DyanmoDBの作成
    //**************************************************** */
    const table: Table = new Table(this, "my-table-id", {
      partitionKey: {
        name: "meeting_id",
        type: AttributeType.NUMBER
      },
      sortKey: {
        name: "password",
        type: AttributeType.STRING
      },
      readCapacity: 1,
      writeCapacity: 1,
      tableName: tableName,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    //**************************************************** */
    //LambdaFunctionの作成
    //**************************************************** */
    const scanMeetingFunction: Function = new Function(this, 'scan-meeting', {
      functionName: 'scan-meeting',
      runtime: Runtime.NODEJS_12_X,
      code: AssetCode.fromAsset('src/lambda'),
      handler: 'scan-meeting.handler',
      timeout: cdk.Duration.seconds(10),
      environment: {
        TZ: "Asia/Tokyo",
        TABLE_NAME: table.tableName,
        CORS_URL: "*" //作成したCloudFrontのエンドポイントを指定する
      },
      logRetention: RetentionDays.TWO_MONTHS,
    });

    const registMeetingFunction: Function = new Function(this, 'regist-meeting', {
      functionName: 'regist-meetings',
      runtime: Runtime.NODEJS_12_X,
      code: AssetCode.fromAsset('src/lambda'),
      handler: 'regist-meeting.handler',
      timeout: cdk.Duration.seconds(10),
      environment: {
        TZ: "Asia/Tokyo",
        TABLE_NAME: table.tableName,
        CORS_URL: "*" //作成したCloudFrontのエンドポイントを指定する
      },
      logRetention: RetentionDays.TWO_MONTHS,
    });

    table.grantFullAccess(scanMeetingFunction);
    table.grantFullAccess(registMeetingFunction);

    //**************************************************** */
    // Cognitoユーザープール・アプリクライアントの作成
    //**************************************************** */
    const userPool: cognito.UserPool = new cognito.UserPool(this, 'your-user-pool-id', {
      userPoolName: "yourUserPoolName",
      // パスワードポリシー
      passwordPolicy: {
        // 4種8桁を定義
          minLength: 8,
          requireLowercase: true,
          requireDigits: true,
          requireUppercase: true,
          requireSymbols: false,
          tempPasswordValidity: cdk.Duration.days(7), // 仮パスワードの有効期限
      },
      selfSignUpEnabled: true,
      // 必須の標準属性やカスタム属性
      standardAttributes: {
        email: {
          required: true,
          mutable: true // 後に値を変更することを許可する
        },
        fullname: {
          required: true,
          mutable: true
        },
      },
      // Cognitoがユーザーのサインアップ時に自動的に確認するために調べる属性
      autoVerify: {
        email: true
      },
      // ユーザーがユーザープールに登録またはサインインする方法
      signInAliases: {
        email: true,
        username: true
      },
      // サインインエイリアスを大文字と小文字を区別して評価するかどうか
      signInCaseSensitive: true,
      // ユーザーは自分のアカウントをどのように回復できるか
      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
      // emailの設定
      // emailSettings: {
      //   from: '',
      //   replyTo: ''
      // },
      // 認証メール設定
      userVerification: {
        emailSubject: 'Your verification code',
        emailBody: 'Your verification code is {####}',
        emailStyle: cognito.VerificationEmailStyle.CODE,
      }
    });

    new cognito.UserPoolClient(this, 'your-user-pool-client-id', {
      userPoolClientName: 'yourAppClientName',
      userPool: userPool,
      // ユーザーによる認証を許可する
      authFlows: {
        refreshToken: true,
        userPassword: true,
        userSrp: true
      },
      // クライアントシークレットを生成する
      generateSecret: true,
      // クライアントがアプリと対話するためのOAuth設定
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
        },
        scopes: [cognito.OAuthScope.EMAIL],
      },
      // ユーザーがユーザープールに存在しない場合(false)、CognitoがUserNotFoundException例外を返すか、またはユーザーの不在を明らかにしない別のタイプのエラーを返すか
      preventUserExistenceErrors: true,
    });

    //**************************************************** */
    // API Gateway(リソース, メソッド)の作成
    //**************************************************** */
    const api = new RestApi(this, "schedule-manager-api", {
      restApiName: restApiName,
      cloudWatchRole: true,

    });
    const scanMeeting = api.root.addResource("scan-meeting");

    const scanMeetingLambdaIntegration = new LambdaIntegration(scanMeetingFunction);
    scanMeeting.addMethod("POST", scanMeetingLambdaIntegration);
    addCorsOptions(scanMeeting);

    const registMeeting = api.root.addResource("regist-meeting");

    const registMeetingLambdaIntegration = new LambdaIntegration(registMeetingFunction);
    registMeeting.addMethod("POST", registMeetingLambdaIntegration);
    addCorsOptions(registMeeting);
  }
}

//**************************************************** */
// API GatewayのメソッドにOPTIONを追加
//**************************************************** */
export function addCorsOptions(apiResource: IResource) {
  apiResource.addMethod(
    "OPTIONS",
    new MockIntegration({
      integrationResponses: [
        {
          statusCode: "200",
          responseParameters: {
            "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
            "method.response.header.Access-Control-Allow-Origin": "'*'",
            "method.response.header.Access-Control-Allow-Credentials": "'false'",
            "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE'",
          },
        },
      ],
      passthroughBehavior: PassthroughBehavior.NEVER,
      requestTemplates: {
        "application/json": '{"statusCode": 200}',
      },
    }),
    {
      methodResponses: [
        {
          statusCode: "200",
          responseParameters: {
            "method.response.header.Access-Control-Allow-Headers": true,
            "method.response.header.Access-Control-Allow-Methods": true,
            "method.response.header.Access-Control-Allow-Credentials": true,
            "method.response.header.Access-Control-Allow-Origin": true,
          },
        },
      ],
    }
  );
}

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

アルバムコレクションの脆弱性

執筆背景

アルバムコレクションという画像共有サイトがあるのだが恐らく作者の意図に反して、誰でも(ユーザ登録していなくても)ファイルをダウンロードできてしまう。
AWSの認証周りが問題なのだが、勉強する上で良い題材なので何がまずいのかを解説する。
※執筆時点9/20の状況です

記事の中身

  • 認証なしでファイルをダウンロードする手順の解説
  • 何がまずかったのかの解説
  • 改善案の提示

認証なしでファイルをダウンロードする手順の解説

想定環境

windows10

使うツール

手順

  1. git for windowsをインストールする(選択肢は全てデフォルトでOK)
  2. Google Chromeをインストールする
  3. Google Chromeでアルバムコレクションにアクセスする
  4. ログインせずに受け取る画面でIDを入力して受け取るボタンを押下する image.png
  5. 出力されたモザイク掛かった画像を右クリックし「検証」を押下する image.png
  6. imgと書いてある部分を右クリックして「Copy」→「Copy outerHTML」を押下する image.png
  7. 適当なエディタにコピーした文字を張り付けると下記のような文字列を得ることができる image.png
  8. 文字列の中からhttps以降thumbより前の(アンダーバー)までを抜き出す image.png ちなみにの後に1をつけてURL欄に張り付けると1番目の画像にアクセスできることが分かる。 image.png 後続の手順では複数ファイルをまとめて取得するため用意している。
  9. PCに適当なフォルダを作り、フォルダ内で右クリックし「Git Bash Here」を押下する image.png
  10. gitターミナルからcurlコマンドを発行する。[]の中の数字はファイル数を表しており、例えば19ファイルであれば[1-19]とする
# curl -O 手順8で抽出した文字列[1-ファイル数]
# 例.ファイル数が19個、手順8の文字列が右記URLの時https://s3-us-west-2.amazonaws.com/file.album-collection.net/a3928lsadgd6e498712c3tirca4ba8b5c3b4974
curl -O https://s3-us-west-2.amazonaws.com/file.album-collection.net/a3928lsadgd6e498712c3tirca4ba8b5c3b4974_[1-19]

gitターミナルを開いたフォルダにファイルが保存されているので確認する
そのままだとファイル名が長すぎるのと、拡張子がうまく付いていないので下記コマンドを入れる。
ファイル名を変えているのとファイルサイズに応じて拡張子(jpgまたはmp4)を付けている

i=1
for file in `ls`
do
  size=`ls -l $file | awk '{print $5}'`
  if [ $size -lt 300000 ] ; then
    mv ${file} ${i}.jpg
  else
    mv ${file} ${i}.mp4
  fi
  i=$((i+1))
done
exit

何がまずかったのか

2つの悪さがあると考えられる

  1. アクセス制御がない(URLが分かればファイルが取得できてしまう)
    ファイルにアクセス制御がなく誰にでも公開された状態になってしまっている。URL情報からS3を使っていることが明確だが、折角のS3なので署名付きURLなどを使いアクセス制御すべきであるが、それがないところが問題。

  2. アクセス先URLが容易に推測できる
    たとえアクセス制御がなかったとしてもアクセス先URLが分からなければ容易にはファイルにたどり着けない。本件ではアクセス先URLが容易に推測できてしまうところが問題。

改善案の提示

ほぼ上記で記載している内容だが、最善策はS3の署名付きURLを使ってアクセス制御をすると誰でもファイルが取得できる状態が回避できる。
次策はアクセス先URLの生成を工夫し、推測できないようなURLを生成することを提案する。
また、ビジネスよりの話になるが、利用規約に同意していない人がファイルをダウンロードできてしまう状態なので、モザイク版についても利用規約に同意した人だけが実施できるようAPIGatewayの認可の仕組みを取り入れるという改善の余地があると考えている。

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

AWS認定12種を取得したので勉強法を紹介してみる

はじめに

先日、2020年9月時点で受験できる AWS 認定の12冠を達成したので、自分も勉強法を紹介してみることにしました。

勉強法を調べてみると、Black Belt やホワイトペーパー、公式クラスルームトレーニング、AWS パートナーのトレーニング、書籍など、多くの方法が出てきます。選択肢が多いのはありがたいですが、それはそれで、どれから始めればいいのかとか、どこまでやればいいのかとかを考えると、なんとなく面倒になってしまう人もいるのではないでしょうか。

私自身、アソシエイト系を取得したところで勉強法に困り中断していましたが、Whizlabs というサイトに全認定の問題集があることを知り、主にこれと Black Belt を使って勉強を再開してみることにしました。結果的に良いペースで認定の取得ができたので、この問題集と勉強法を紹介してみようと思います。

対象者

AWS をそれなりに使っていて、これから認定試験を受けようと考えている方が対象です。

なお、紹介する Whizlabs の問題集はすべて英語なので、英語を読むのがあまり得意でない方は、後述する無料のお試し問題でチェックしてみるのが良いかもしれません。

AWS 歴

AWS 歴=エンジニア歴は4年目で、ここ1年半くらいはサーバサイドエンジニアとして AWS 環境の設計、構築、運用をしています。これまでに触ったことのあるサービスは、チュートリアルレベルを含めて50個ほどです。

取得履歴

1年ほどかけて13個の認定を取得しました(ビッグデータ - 専門知識 は廃止されて データ分析 - 専門知識 に変わったため、タイトルでは12種としています)。
5月以降に取得した認定は今回紹介する Whizlabs の問題集で学習しています。コロナでテレワークになり平日に時間を多く使えるようになったので、業務終了後に勉強して週1くらいのペースで受験するようにしていました。

認定 取得日 スコア
クラウドプラクティショナー 2019-10-02 927
ソリューションアーキテクト – アソシエイト 2019-10-02 909
デベロッパー – アソシエイト 2019-10-09 845
SysOps アドミニストレーター – アソシエイト 2019-10-09 925
セキュリティ – 専門知識 2020-05-10 846
高度なネットワーキング – 専門知識 2020-05-10 72%
ソリューションアーキテクト – プロフェッショナル 2020-05-22 916
DevOps エンジニア – プロフェッショナル 2020-05-29 840
(ビッグデータ – 専門知識) 2020-06-08 80%
データ分析 – 専門知識 2020-06-14 813
データベース – 専門知識 2020-06-19 774
機械学習 – 専門知識 2020-08-07 913
Alexa スキルビルダー – 専門知識 2020-08-15 884

Whizlabs とは

資格試験などのいろいろな学習コンテンツを提供しているサイトです。
AWS 認定試験のコンテンツは、 Practice Tests (問題集)と、 Online Course (ビデオとハンズオンラボ)の2種類が提供されていて、すべて英語です。後者は使ったことがないので今回は触れません。

以下の画像は Practice Tests の無料のお試し版である Free Test を練習モード(時間制限なし、答えがすぐ見れる)で開始したときの問題と選択肢の画面です。実際の試験と同じように、4択程度の択一問題と複数選択問題があります。

image.png

Show Answer を押すと解説と参考リンクが表示されます。参考リンクは多くの場合 AWS の公式ドキュメントへのリンクですが、 medium.com などの記事のこともあります。
image.png

問題の傾向は、本番試験と非常によく似た問題もあればちょっと外れてるかなというものもありました。しかし、本番で全く知らない内容が出ることはほとんどなかったので、少なくとも私が使った専門知識とプロフェッショナルの問題集は重要なポイントをだいたいカバーしていたのではないかと思います。

ひとつ注意点をあげるとすると、Whizlabs はたびたび問題文が冗長で、前置きが長々と書かれているわりにあまり設問に関係なかったりします。実際の試験も長文はあるにはありますがもっと簡潔で、当たり前ですが AWS 公式の模擬試験に近いです。問題の雰囲気はそちらを参考にしたほうが良いと思います。

Practice Tests の問題数と料金

2020/09/26 時点の情報です。

問題数

全認定の問題集が揃っています。問題数は 155〜944 問と、認定によって結構な差があります。

認定 問題数
クラウドプラクティショナー 300
ソリューションアーキテクト – アソシエイト 760
デベロッパー – アソシエイト 944
SysOps アドミニストレーター – アソシエイト 805
ソリューションアーキテクト – プロフェッショナル 535
DevOps エンジニア – プロフェッショナル 440
セキュリティ – 専門知識 315
高度なネットワーキング – 専門知識 650
(ビッグデータ – 専門知識) 418
データ分析 – 専門知識 215
データベース – 専門知識 155
機械学習 – 専門知識 155
Alexa スキルビルダー – 専門知識 175

料金

試験のレベルごとに料金が設定されています。
今なら Stay Safe & Learn from Home キャンペーンということで、全コース50%オフの何回でも使えるクーポンコードがあります。

※ 下表はクーポン適用前の料金です

レベル 料金
クラウドプラクティショナー $ 15.95
アソシエイト $ 24.95
プロフェッショナル $ 29.95
専門知識 $ 39.95

勉強法

以下の流れで勉強しました。

  1. 試験概要ページ試験ガイドを読んで試験内容をざっくりと把握する(リンクは ソリューションアーキテクト プロフェッショナル の例)
  2. AWS 公式のサンプル問題(10問)を解く
  3. AWS 公式の模擬試験(20問)を解く
  4. 3,4で出てきた AWS サービスの Black Belt をすべて読む
  5. メモを取りながら問題集を一通り解く
  6. メモを見て復習し、心配なら問題集をもう一度解く
  7. 受験する

Whizlabs のみ紹介しましたが、実際は koiwaclub.com の日本語問題集も併用しています(アソシエイトは Whizlabs を知る前だったため koiwaclub のみ、プロフェッショナルは両方、専門知識は Whizlabs のみ)。
日本語で取っ付きやすいので、まずは koiwaclub で受けたい試験の問題がないか確認したほうが良いかもしれません。

また、Alexa スキルビルダー は問題集だけだとイメージが掴みにくかったので、Alexa Developers JP チャンネル - YouTubeAlexa 道場 シリーズを一通り視聴しました。説明がとても丁寧でわかりやすいのでおすすめです。

Black Belt の読み方

Black Belt の公開資料には、動画と、動画内で説明に使われているスライド(SlideShare / PDF)がありますが、スライドだけ見ました。サービス数が多く、1サービスが複数に分かれていることも多いので、動画を見ているとかなり時間がかかります。
まったく知らないサービスでも読めば理解できるように書かれているものがほとんどだったので、スライドだけでも大丈夫だと思います。

問題集の解き方

問題集は、理解を確かめるためというよりは、重要ポイントを把握して関連ドキュメントを参照するためのものとして使いました。

Black Belt を読んでも知らない内容はいくらでもあるので、わからなそうならすぐに答えを開いて解説を読み要点を理解します。それだけだと不十分なことも多いので参考ドキュメントを見て、分かりづらい場合はさらに他の解説記事を探します。AWS の公式ドキュメントは分量が多く英語ページしかないことも多々あるので、Qiita や Developers.IO の適度なボリュームでわかりやすくまとめられている記事に何度も助けられました。

また、わからなかった部分は復習用としてメモを取りながら解くようにしました。Whizlabs には単語帳アプリのように間違えた問題だけ解き直す機能がないので、あとで効率よく見返すためにはメモしたほうが良いです。

受験のタイミング

プロフェッショナル2種は Whizlabs の問題集が8割くらい解けたので1周目を最後までやりきらずに受験しましたが、どちらも本番では8割以上取ることができました。問題の傾向は多少違っていても、おおむね同じくらいの難しさだったのではと思います。
専門知識も本番と大きく難易度が違う印象は受けなかったので、あくまで参考程度としてですが、Whizlabs の問題が7~8割くらい解ければ受験を考えても良いのかもしれません。

まとめ

Whizlabs の問題集や Black Belt を使った勉強法を紹介しました。

問題集を軸にすることで、膨大な AWS のドキュメントから特に重要な部分だけを簡単に参照することができ、効率良く進めることができました。知らないサービスを実際に触ってみたり、ベストプラクティスをきちんと学んだりする勉強法ではありませんが、その分、消耗しすぎずモチベーションも保てたのではないかなと思っています。

紹介した内容が AWS 認定の勉強法に困っている方の参考になれば幸いです。

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