Amazon Web Services ブログ

コンテナまで Transport Layer Security を維持: Amazon ECS および Envoy で Application Load Balancer を使用する



この記事は、Sri Mallu、Re Alvarez-Parmar、Sahil Thapar が寄稿したものです

Application Load Balancer は、高可用、スケーラブルで安全なアプリケーションを構築する際、重要な要素となっています。AWS のお客様は、ALB を利用することで、従来からアプリケーションのコードで使えた機能を実行できます。接続のセキュリティを例にとると、ALB を使用して暗号化と復号化の作業をオフロードできるため、アプリケーションのビジネスロジックに集中できます。

Application Load Balancer を使用すると、暗号化された接続 (SSL オフロードとも呼ばれる) を使用する HTTPS リスナーを作成できます。この機能により、ロードバランサーと、SSL または TLS セッションを開始するクライアント間のトラフィックの暗号化が可能になります。HTTPS リスナーを作成するときは、SSL/TLS サーバー証明書をロードバランサーにデプロイします。ロードバランサーは、このサーバー証明書を使用してフロントエンド接続を終了し、クライアントからのリクエストを復号化してからターゲットに送信します。ALB がリクエストを復号化する必要があるのはなぜでしょうか?

ALB がリクエストを復号化する必要があるのは、それが Open Systems Interconnection (OSI) モデルのアプリケーション層で動作していて、リクエストをルーティングするにはリクエストを検査する必要があるためです。リクエストをルーティングするには、HTTP ヘッダー、メソッド、クエリパラメータ、ソース IP CIDR に基づいて ALB を使用できます。ALB を使用してアプリケーションの負荷を分散すると、ALB で SSL/TLS が終了するのはそのためで、通常、ALB とバックエンドアプリケーション間の接続は暗号化されません。ロードバランサーで安全な接続を終了し、バックエンドで HTTP を使用するだけで、お使いのアプリケーションにとっては十分かもしれません。AWS リソース間のネットワークトラフィックは、接続の一部であるインスタンスのみがリッスンできるからです。

ただし、厳しい外部規制に準拠する必要があるアプリケーションを開発している場合は、すべてのネットワーク接続を保護する必要があることがあります。

以下、クライアントとロードバランサーの間、およびロードバランサーから ECS クラスターで実行されているアプリケーションコンテナへの接続を暗号化する方法を示します。この記事では、Fargate の ECS コンテナを実行していますが、ソリューションは EC2 で実行されている ECS コンテナにも使えます。この記事でご紹介する手順を実行するには、パブリックサブネットとプライベートサブネット、適切なルートテーブル、インターネットゲートウェイ、および NAT ゲートウェイを持つ VPC が必要になります。

また、TLS を終了するフロントプロキシとして Envoy を使用し、アプリケーションコンテナと共に Envoy をサイドカーとして実行します。この方法では、アプリケーションコードで暗号化を処理する必要はありません。Envoy は、暗号化されていないトラフィックを、localhost を介してアプリケーションコンテナに送信します。

前提条件

概説した手順を正常に実行するには、以下をご確認ください。

  • 最新の AWS CLI がインストールおよび設定されていること。
  • Docker および OpenSSL がインストールされていること。

アーキテクチャ

このテンプレートをセットアップのベースラインとして使用できます。

まず、自己署名証明書を作成します。これは、Envoy コンテナイメージに埋め込まれます。次に、同じプライベートキーと証明書を使用して ACM 証明書を作成し、ACM にインポートします。この ACM 証明書を ALB でサーバー証明書として使用します。

このソリューションでは自己署名証明書を使用しますが、ACM プライベート CA を使用して証明書を生成することもできます。このソリューションではプライベートキーをエクスポートできないため、ACM パブリック認証機関 (CA) を使用できません。ただし、プライベートキーをエクスポートする市販の信頼できる CA を使用できます。

自己署名証明書を使用しているため、ブラウザからアプリケーションにアクセスする場合は、ブラウザで信頼できるアプリケーションとして設定する必要があります。商用の信頼できる CA を使用する場合、これは必要ありません。

チュートリアル

チュートリアル全体で使用されるいくつかの環境変数を見てみましょう。

##Export region and account
export account=<account> # <- Your account number here
export AWS_REGION=<AWS Region> # <- Your AWS Region

##Export key networking constructs
export private_subnet1="subnet-0cb23e7b2da6116ec" # Subsitute these values with your VPC subnet ids
export private_subnet2="subnet-01446062d07790b98"
export public_subnet1="subnet-092e36d83b9d0fd51"
export public_subnet2="subnet-0a41fcbbe7add4b76"
export sg=sg-0ea3f8730146cc784   ##Wide open SG for ALB All All 0.0.0.0/0
export vpcId=vpc-0a0598d1d7d1dd8b3 # <- Change this to your VPC id

##Service name and domain to be used
export service_name=ecs-encryption
export dns_namespace=awsblogs.info  ##This should be your domain

上記の CloudFormation テンプレートを使用している場合は、ECS タスク実行 IAM ロールが作成されます。自分でロールを作成する場合は、次の権限と信頼ポリシーがあることを確認してください。

##Policy
{
    "Statement": [
        {
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

##Trust
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

ecsTaskExecutionRoleArn 環境変数を作成します。

export ecsTaskExecutionRoleArn=arn:aws:iam::551961765653:role/ECS-ENCRYPTION-ECSTaskExecutionRole-URQRCO2HC4E3

ECR リポジトリの設定

アプリケーションと Envoy コンテナイメージを格納する 2 つの ECR リポジトリを作成します。

リポジトリ 1:

aws ecr create-repository \
--repository-name ${service_name}-blog-app \
--region $region

出力:

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-west-1:<account>:repository/ecs-end-end-encryption-blog-app",
        "registryId": "<account>",
        "repositoryName": "ecs-end-end-encryption-blog-app",
        ....
}

リポジトリ 2:

export aws_ecr_repository_url_app=$account.dkr.ecr.$region.amazonaws.com/${service_name}-blog-app

aws ecr create-repository --repository-name ${service_name}-blog-proxy \
--region $region

出力:

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-west-1:<account>:repository/ecs-end-end-encryption-blog-proxy",
        "registryId": "<account>",
        "repositoryName": "ecs-end-end-encryption-blog-proxy",
        ...
}
export aws_ecr_repository_url_proxy=$account.dkr.ecr.$region.amazonaws.com/${service_name}-blog-proxy

証明書のセットアップ

キーペアを作成して、ACM 証明書としてインポートしましょう。この証明書を ALB に関連付けます。
同じキーと証明書を使用して、Envoy プロキシで TLS 暗号化を有効にします。

mkdir -p docker/certs && cd docker/certs

##Create the config
cat <<EOF > castore.cfg
[ req ]
default_bits = 2048
default_keyfile = my-aws-private.key
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[ req_distinguished_name ]
C = US
ST = VA
L = Richmond
O = awsblogs.info
OU = awsblogs.info
CN= ecs-encryption.awsblogs.info ## Use your domain
emailAddress = user@email.com ## Use your email address
[v3_ca]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = CA:true
[v3_req]
## Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
EOF

OpenSSL を使用して証明書署名機関を作成し、それを使用してプライベートキーと証明書を生成します。

openssl genrsa -out castore.key 2048
openssl req -x509 -new -nodes -key castore.key -days 3650 -config castore.cfg -out castore.pem

openssl genrsa -out my-aws-private.key 2048
openssl req -new -key my-aws-private.key -out my-aws.csr -config castore.cfg
openssl x509 -req -in my-aws.csr -CA castore.pem -CAkey castore.key -CAcreateserial  -out my-aws-public.crt -days 365
aws acm import-certificate \
--certificate file://my-aws-public.crt \
--private-key file://my-aws-private.key \
--certificate-chain  file://castore.pem \
--region $region

出力:

{
    "CertificateArn": "arn:aws:acm:us-west-1:<account>:certificate/838bca7e-d33b-41e1-b590-97253cd6e506"
}

証明書 ARN をエクスポートします。

export certificateArn=arn:aws:acm:us-west-1:<account>:certificate/838bca7e-d33b-41e1-b590-97253cd6e506

証明書がインポートされたことを ACM コンソールで確認してみましょう。

Docker イメージをビルドして ECR にプッシュする

Envoy プロキシ用とデモ hello アプリケーション用の 2 つの Docker イメージを作成してみましょう。後でタスク定義を作成し、このコンテナのペアを定義します。Envoy プロキシはサイドカーとして実行され、localhost を介して hello アプリケーションコンテナにリクエストをルーティングします。

次の内容で、単純な hello サービスを定義します。

cd docker

cat <<EOF > service.py
from flask import Flask
import socket

app = Flask(__name__)

@app.route('/service')
def hello():
  return (f'Hello from behind Envoy proxy!!\n')

if __name__ == "__main__":
  app.run(host='0.0.0.0', port=8080, debug=True)
EOF

アプリケーションコンテナ用の Dockerfile を作成します。

cat <<EOF > Dockerfile-app
FROM envoyproxy/envoy-alpine-dev:latest

RUN apk update && apk add python3 bash curl; \
      pip3 install -q Flask==0.11.1 requests==2.18.4; \
      mkdir /code

ADD ./service.py /code

EXPOSE 8080

CMD ["python3", "/code/service.py"]
EOF

Envoy 設定ファイルを作成します。Envoy 設定ファイルは、リクエストのプロキシとルーティングに使用します。ALB からのすべてのリクエストは、TLS で暗号化されます。プロキシは、リクエストを HTTP 経由でアプリケーションコンテナにルーティングします。

cat <<EOF > envoy.yaml
static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 443
    filter_chains:
      tls_context:
        common_tls_context:
          tls_certificates:
          - certificate_chain:
              filename: "/etc/ssl/my-aws-public.crt"
            private_key:
              filename: "/etc/ssl/my-aws-private.key"
      filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: service
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: local_service
          http_filters:
          - name: envoy.router
            config: {}
  clusters:
  - name: local_service
    connect_timeout: 0.5s
    type: strict_dns
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: 127.0.0.1
        port_value: 8080

admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8081
EOF

Envoy を実行するための起動スクリプトを作成します。

cat <<EOF > start_envoy.sh
#!/bin/sh
/usr/local/bin/envoy -c /etc/envoy.yaml
EOF

Envoy プロキシ用の Dockerfile を作成します。

cat <<EOF > Dockerfile-proxy
FROM envoyproxy/envoy-dev:latest

RUN apt-get update && apt-get -q install -y \
    curl wget jq python \
        python-pip \
        python-setuptools \
        groff \
        less \
        && pip --no-cache-dir install --upgrade awscli
RUN mkdir -p /etc/ssl
ADD start_envoy.sh /start_envoy.sh
ADD envoy.yaml /etc/envoy.yaml
ADD certs /etc/ssl/

RUN chmod +x /start_envoy.sh

ENTRYPOINT ["/bin/sh"]
EXPOSE 443
CMD ["start_envoy.sh"]
EOF

Docker イメージをビルドして ECR にプッシュしましょう。

## Build images locally, make sure you are in the docker folder
docker build -t ${aws_ecr_repository_url_proxy} -f Dockerfile-proxy .
docker build -t ${aws_ecr_repository_url_app} -f Dockerfile-app .

## Verify
docker images | grep $region
 
## Login to ECR
aws ecr get-login --region $region

## Grab the password from output of previous command and execute the following
docker login -u AWS -p <password> https://$account.dkr.ecr.$region.amazonaws.com
Login Succeeded

## Push to ECR
docker push ${aws_ecr_repository_url_proxy}
docker push ${aws_ecr_repository_url_app}

画像が ECR にプッシュされていることを確認します。

クラスターとタスク定義を作成する

両方のコンテナ定義を使用してタスク定義を作成します。

cat <<EOF >  ecs_task_def.template
{
   "containerDefinitions": [
      {
       "logConfiguration": {
         「logDriver」: "awslogs"、
         "options": {
           "awslogs-group": "/ecs/$service_name",
           "awslogs-region": "$region",
           "awslogs-stream-prefix": "ecs"
         }
       },
       "portMappings": [
         {
           "hostPort": 443,
           "protocol": "tcp",
           "containerPort": 443
         }
       ],
       "cpu": 0,
       "environment": [
         {"name":  "DNS_NAME", "value":  "$service_name.awsblogs.info"}
       ],
       "image": "$aws_ecr_repository_url_proxy:latest",
       "name": "envoy"
     },
     {
       "logConfiguration": {
         「logDriver」: "awslogs"、
         "options": {
           "awslogs-group": "/ecs/$service_name",
           "awslogs-region": "$region",
           "awslogs-stream-prefix": "ecs"
         }
       },
       "cpu": 0,
       "image": "$aws_ecr_repository_url_app:latest",
       "name": "service"
     }
   ],
   "cpu": "256",
   "taskRoleArn": "$ecsTaskExecutionRoleArn",
   "executionRoleArn": "$ecsTaskExecutionRoleArn",
   "family": "$service_name",
   "memory": "512",
   "networkMode": "awsvpc",
   "requiresCompatibilities": [ 
       "FARGATE" 
    ]

}
EOF

環境変数を置き換え、ロググループと ECS クラスターを作成し、タスク定義を登録します。

envsubst <ecs_task_def.template>ecs_task_def.json

export log_group_name=/ecs/ecs-end-end-encryption

aws logs create-log-group \
--log-group-name $log_group_name \
--region $region

export cluster=${service_name}-cluster

aws ecs create-cluster --cluster-name $cluster \
--region $region

出力:

{
    "cluster": {
        "clusterArn": "arn:aws:ecs:us-west-1:<account>:cluster/ecs-encryption-cluster",
        "clusterName": "ecs-encryption-cluster",
        "status": "ACTIVE",
        "registeredContainerInstancesCount": 0,
        "runningTasksCount": 0,
        "pendingTasksCount": 0,
        "activeServicesCount": 0,
        "statistics": [],
        "tags": [],
        "settings": [
            {
                "name": "containerInsights",
                "value": "disabled"
            }
        ],
        "capacityProviders": [],
        "defaultCapacityProviderStrategy": []
    }
}

タスク定義を登録します。

aws ecs register-task-definition \
--cli-input-json file://ecs_task_def.json \
--region=$region

AWS マネジメントコンソールでタスク定義の作成を確認します。

Application Load Balancer を作成する

Application Load Balancer を作成し、アプリケーションのリスナールールとターゲットグループをセットアップします。

export alb=${service_name}-alb

aws elbv2 create-load-balancer --name $alb \
--scheme internet-facing \
--subnets $public_subnet1 $public_subnet2 \
--security-groups $sg --region $region

出力:

{
    "LoadBalancers": [
        {
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-1:<account>:loadbalancer/app/ecs-encryption-alb/420cd49c9b77c43b",
            "DNSName": "ecs-encryption-alb-860337044.us-west-1.elb.amazonaws.com",
            ....
        }
    ]
}
export loadbalancerArn=arn:aws:elasticloadbalancing:us-west-1:<account>:loadbalancer/app/ecs-encryption-alb/420cd49c9b77c43b 

aws elbv2 create-target-group \
--name https-target \
--protocol HTTPS \
--port 443 \
--health-check-path /service \
--target-type ip \
--vpc-id $vpcId \
--region $region

出力:

{
    "TargetGroups": [
        {
            "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-1:<account>:targetgroup/https-target/3e6bed06eed53f98",
            "TargetGroupName": "https-target",
            "Protocol": "HTTPS",
            "Port": 443,
            "VpcId": "vpc-0a0598d1d7d1dd8b3",
            ...
    ]
}
export targetGroupArn=arn:aws:elasticloadbalancing:us-west-1:<account>:targetgroup/https-target/3e6bed06eed53f98

aws elbv2 create-listener --load-balancer-arn $loadbalancerArn \
--protocol HTTPS --port 443  \
--certificates CertificateArn=$certificateArn \
--default-actions Type=forward,TargetGroupArn=$targetGroupArn \
--region $region

出力:

{
    "Listeners": [
        {
            "ListenerArn": "arn:aws:elasticloadbalancing:us-west-1:<account>:listener/app/ecs-encryption-alb/420cd49c9b77c43b/c234e7efab3456d9",
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-1:<account>:loadbalancer/app/ecs-encryption-alb/420cd49c9b77c43b",
            "Port": 443,
            "Protocol": "HTTPS",
            "Certificates": [
                {
                    "CertificateArn": "arn:aws:acm:us-west-1:<account>:certificate/838bca7e-d33b-41e1-b590-97253cd6e506"
                }
            ],
            "SslPolicy": "ELBSecurityPolicy-2016-08",
            "DefaultActions": [
                {
                    "Type": "forward",
                    "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-1:<account>:targetgroup/https-target/3e6bed06eed53f98",
                    "ForwardConfig": {
                        "TargetGroups": [
                            {
                                "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-1:<account>:targetgroup/https-target/3e6bed06eed53f98",
                                "Weight": 1
                            }
                        ],
                        "TargetGroupStickinessConfig": {
                            "Enabled": false
                        }
                    }
                }
            ]
        }
    ]
}

サービスの作成

ECS サービス定義テンプレートを作成します。ファイルの値をアカウントに一致するように置き換えます。

cat <<EOF >  ecs_service_def.template
{
    "serviceName": "$service_name-service",
    "cluster": "arn:aws:ecs:$region:$account:cluster/$cluster",
    "taskDefinition": "arn:aws:ecs:$region:$account:task-definition/$service_name",
    "loadBalancers": [
                {
                    "targetGroupArn": "$targetGroupArn",
                    "containerName": "envoy",
                    "containerPort": 443
                }
            ],
    "launchType": "FARGATE",
    "platformVersion": "LATEST",
    "networkConfiguration": {
                "awsvpcConfiguration": {
                    "subnets": [
                        "$private_subnet1", "$private_subnet2"
                    ],
                    "securityGroups": [
                        "$sg"
                    ],
                    "assignPublicIp": "ENABLED"
                }
            },
    "deploymentConfiguration": {
                "maximumPercent": 200,
                "minimumHealthyPercent": 100
            },
    "desiredCount": 2,
    "healthCheckGracePeriodSeconds": 0,
    "schedulingStrategy": "REPLICA",
    "enableECSManagedTags": false,
    "propagateTags": "NONE"
    
}
EOF

登録済みのタスク定義と Application Load Balancer を使用して、ECS サービスを作成します。

envsubst <ecs_service_def.template>ecs_service_def.json

##create service 
aws ecs create-service --cluster $cluster \
--service-name ${service_name}-service \
--cli-input-json file://ecs_service_def.json \
--region $region

AWS マネジメントコンソールでサービスを確認します。

DNS のセットアップ

Route 53 でホストするドメインに Route 53 レコードセットをセットアップしましょう。ホストゾーンを作成しました。レコードセットを作成して、先ほど作成した ALB にポイントします。このドメインの証明書に共通名 (CN) を設定しました [CN= ecs-end-end-encryption.awsblogs.info]

テスト

curl コマンドを使用して、アプリケーションで TLS ハンドシェイクをテストしてみましょう。

echo quit | openssl s_client -showcerts -servername ecs-encryption.awsblogs.info -connect ecs-encryption.awsblogs.info:443 > cacert.pem

出力:

depth=0 C = US, ST = VA, L = Richmond, O = awsblogs.info, OU = awsblogs.info, CN = ecs-encryption.awsblogs.info, emailAddress = user@email.com
verify error:num=18:self signed certificate
verify return:1
depth=0 C = US, ST = VA, L = Richmond, O = awsblogs.info, OU = awsblogs.info, CN = ecs-encryption.awsblogs.info, emailAddress = user@email.com
verify return:1
DONE
##Hit the service 
curl --cacert cacert.pem https://ecs-encryption.awsblogs.info/service

出力:

Hello from behind Envoy proxy!!

成功したようです。クライアントから ALB へ、および ALB からアプリケーションへ (Envoy フロントプロキシ経由で) トラフィックを暗号化しました。アプリケーションを変更する必要はありませんでした。

ちなみに、Envoy には他にも多くの用途があり、一般的なサービスメッシュの多くは Envoy をデータプレーンに使用しています。暗号化をアプリケーションに透過的に追加したのと同じように、Envoy を使用してアクセスログ、トレース、およびネットワークメトリクスを生成することもできます。Envoy プロキシを個別に管理および設定するのではなく、サービスメッシュによりサービスプロキシを一元管理できます。App Mesh について詳しくは、こちらをご覧ください。

リソース

ECS インフラストラクチャのセットアップ用の AWS Cloud Formation テンプレート
Transport Layer Security の維持
NLB を使用したエンドツーエンドの暗号化
Envoy による ECS のコンテナまでの暗号化

Re Alvarez-Parmar

Re Alvarez-Parmar

Re Alvarez-Parmar は、アマゾン ウェブ サービスのコンテナスペシャリストソリューションアーキテクトです。彼は、AWS のお客様が AWS コンテナサービスを使用して、スケーラブルで安全なアプリケーションを設計するのを支援しています。彼はニューヨークを拠点に活動しており、Twitter で @realz (http://twitter.com/realz) のユーザー名でたまにツイートしています。

Sahil Thapar

Sahil Thapar

Sahil Thapar はエンタープライズソリューションアーキテクトです。彼は AWS クラウド上で高可用性、スケーラブル、復元力のあるアプリケーションを構築するのにお客様と協力してしています。彼は現在、コンテナと機械学習ソリューションに注力しています。