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
を介してアプリケーションコンテナに送信します。
前提条件
概説した手順を正常に実行するには、以下をご確認ください。
アーキテクチャ
このテンプレートをセットアップのベースラインとして使用できます。
まず、自己署名証明書を作成します。これは、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 のコンテナまでの暗号化