シンプルな構成ですとApplication Load Balancer(以下、ALB)をSSL終端としてALB - EC2間はhttp接続するケースを採用されることが多いと思います。
しかし、コンプライアンス上エンドツーエンドで暗号化を設定したいという顧客もいるため、今回はそれらをワンストップで実装できる手段を
ユースケース
但しパフォーマンスなどのリクエストやパフォーマンス重視の場合は、エンドツーエンドでのSSL暗号化通信は不向きでしょう。
環境
今回使用する開発環境の紹介。
下記図の通り、AWS環境上で登録する。
- Node: v18.0.0
- 開発ツール: AWS Cloud Development Kit (AWS CDK) v2 ... ver 2.144.0
アーキテクチャ
本当だったら、EC2をプライベートサブネットに配置して直接インターネットからの接続を遮断する必要はあるのですが、今回はエンドツーエンドの検証目的のため ①Certbotを使用してEC2単体でHTTPS通信を設定した後、② Application Load Balancer を構築してALB経由でのエンドツーエンド通信を設定する予定です。
セットアップ(CDKプロジェクト作成 〜 EC2作成)
https://letsencrypt.org/getting-started/
https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal
先ずはEC2までの環境までを構築してみます。
インフラのプロビジョングはAWS CDK を利用して、開発言語はTypeScriptで開発しています。
CDKのインストールから開始する際は、こちらからお願いします。
CDK のインストールが完了した後、以下のようにCDKプロジェクトを作成します。
mkdir end_to_end_encryption
cd end_to_end_encryption
cdk init app --language typescript
CDKプロジェクトが作成されたら,AWSリソースを定義するため、各コンストラクトを呼び出します。
ひとまずはVPCとEC2の作成と、EC2がHTTP接続できるようになれれば下準備は完了です。
以下のファイルを下記のように修正します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as elbv2tgt from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as r53 from 'aws-cdk-lib/aws-route53';
import * as r53_tgt from 'aws-cdk-lib/aws-route53-targets';
export class SimpleEc2WebappStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/* ---------- IAM Role ---------- */
const roleEC2 = new iam.Role(this, `iam-role-ec2`, {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
});
roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("IAMFullAccess")); // upload-server-certificate実行用に一時的に付与。設定後はコメントアウト
/* ---------- VPC ---------- */
const vpc = new ec2.Vpc(this, `vpc`, {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
// {
// cidrMask: 24,
// name: 'ProtectedApp',
// subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
// },
// {
// cidrMask: 24,
// name: 'PrivateIsolatedDB',
// subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
// },
],
// natGateways: 2,
});
/* ---------- SG(ALB) ---------- */
const sgEc2Alb = new ec2.SecurityGroup(this, `sg-alb`, {
vpc,
});
sgEc2Alb.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
/* ---------- SG(EC2 Web) ---------- */
const sgEc2Web = new ec2.SecurityGroup(this, `sg-ec2`, {
vpc,
});
sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(443));
sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(80)); // CertbotでSSL証明書を導入するため、一時的に解放
// sgEc2Web.addIngressRule(sgEc2Alb, ec2.Port.tcp(443));
/* ---------- EC2 ---------- */
const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
instanceType: new ec2.InstanceType("t3.micro"),
machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
role: roleEC2,
securityGroup: sgEc2Web,
associatePublicIpAddress: true,
});
/* ---------- EIP ---------- */
const eip = new ec2.CfnEIP(this, 'eip', { domain: 'vpc' });
new ec2.CfnEIPAssociation(this, 'eip-associate', {
allocationId: eip.attrAllocationId,
instanceId: ec2Instance.instanceId,
});
/* ---------- ALB ---------- */
// const alb = new elbv2.ApplicationLoadBalancer(this, `alb`, {
// vpc: vpc,
// internetFacing: true,
// securityGroup: sgEc2Alb,
// });
/* ---------- TargetGroup ---------- */
// const tg = new elbv2.ApplicationTargetGroup(this, `tg`, {
// vpc: vpc,
// port: 443,
// targetType: elbv2.TargetType.INSTANCE,
// targets: [
// new elbv2tgt.InstanceIdTarget(ec2Instance.instanceId, 443)
// ],
// });
/* ---------- Listner ---------- */
// alb.addListener(`listener-443`, {
// port: 443,
// defaultTargetGroups: [tg],
// certificates: [
// {
// certificateArn: "arn:aws:iam::123456789012:server-certificate/ssl-cert-letsencrypt-domainname" // Certbot設定後、設定
// }
// ],
// });
/* ---------- R53 HostZone ---------- */
const hostzone = r53.HostedZone.fromLookup(this, `r53`, {
domainName: "example.com",
});
// /* ---------- R53 Record ---------- */
new r53.ARecord(this, `r53-record`, {
zone: hostzone,
recordName: `www.example.com`,
target: r53.RecordTarget.fromIpAddresses(eip.attrPublicIp), // Certbot設定後はコメントアウト
// target: r53.RecordTarget.fromAlias(new r53_tgt.LoadBalancerTarget(alb)),
});
}
}
セキュリティグループは80と443ポートを一時的に解放しています。
80番についてはcertbotコマンドによってサーバへSSL証明書の登録をするため、一時的にAnywhereで登録する。
Certbotセットアップ
これでcertbotによるセットアップは完了です。
certbotから取得した証明書の情報について確認するには、以下のコマンドを実行し、先ほどセットアップした証明書がサーバに登録されていることを確認します。
certbot certificates
https接続すると問題なく接続することができます。
https://letsencrypt.org/ja/certificates/
このタイミングでセキュリティグループで一時的に解放した80番ポートを削除しても問題ないです。
(80番ポートを解放していないくても更新することができたので、おそらく証明書登録以降は443で通信するから?)
証明書の更新は以下の通りです。
ドライランは以下の通り。
certbot renew --dry-run
証明書の更新は以下の通り.
certbot renew
# 証明書の更新は30日を切らないと更新がスキップされる. オプションとして「--force-renewal」を指定して強制的に更新させる
certbot renew --force-renewal
ALB作成 & 証明書登録
証明書は/etc/letsencrypt/live/[ドメイン名]/
に作成されています。
EC2から証明書をIAMに登録します.
https://repost.aws/ja/knowledge-center/import-ssl-certificate-to-iam
cd /etc/letsencrypt/live/[ドメイン名]
# 登録
aws iam upload-server-certificate \
--server-certificate-name ssl-cert-letsencrypt-domainname \
--certificate-body file://cert.pem \
--private-key file://privkey.pem \
--tags '{ "Key": "ExpiredDate", "Value": "202408" }'
# 確認
aws iam list-server-certificates
IAMでのサーバ証明書の管理について詳しくはこちらを参考に。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_server-certs.html
上記が完了したら ALBを作成します。
ALBの作成にあたりリスナールールの追加やRoute 53 のレコード変更(EC2 -> ALBのエイリアスレコード)を実施するため、再度./lib/end_to_end_encryption-stack.ts
を変更し、cdk deploy
を実施しスタックを更新します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as elbv2tgt from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as r53 from 'aws-cdk-lib/aws-route53';
import * as r53_tgt from 'aws-cdk-lib/aws-route53-targets';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
export class SimpleEc2WebappStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/* ---------- IAM Role ---------- */
const roleEC2 = new iam.Role(this, `iam-role-ec2`, {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
});
roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
// roleEC2.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("IAMFullAccess")); // upload-server-certificate実行用に一時的に付与。設定後はコメントアウト
/* ---------- VPC ---------- */
const vpc = new ec2.Vpc(this, `vpc`, {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
// {
// cidrMask: 24,
// name: 'ProtectedApp',
// subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
// },
// {
// cidrMask: 24,
// name: 'PrivateIsolatedDB',
// subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
// },
],
// natGateways: 2,
});
/* ---------- SG(ALB) ---------- */
const sgEc2Alb = new ec2.SecurityGroup(this, `sg-alb`, {
vpc,
});
sgEc2Alb.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
/* ---------- SG(EC2 Web) ---------- */
const sgEc2Web = new ec2.SecurityGroup(this, `sg-ec2`, {
vpc,
});
// sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(443));
// sgEc2Web.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(80)); // CertbotでSSL証明書を導入するため、一時的に解放
sgEc2Web.addIngressRule(sgEc2Alb, ec2.Port.tcp(443));
/* ---------- EC2 ---------- */
const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
instanceType: new ec2.InstanceType("t3.micro"),
machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
role: roleEC2,
securityGroup: sgEc2Web,
associatePublicIpAddress: true,
});
/* ---------- EIP ---------- */
const eip = new ec2.CfnEIP(this, 'eip', { domain: 'vpc' });
new ec2.CfnEIPAssociation(this, 'eip-associate', {
allocationId: eip.attrAllocationId,
instanceId: ec2Instance.instanceId,
});
/* ---------- ALB ---------- */
const alb = new elbv2.ApplicationLoadBalancer(this, `alb`, {
vpc: vpc,
internetFacing: true,
securityGroup: sgEc2Alb,
});
/* ---------- TargetGroup ---------- */
const tg = new elbv2.ApplicationTargetGroup(this, `tg`, {
vpc: vpc,
port: 443,
targetType: elbv2.TargetType.INSTANCE,
targets: [
new elbv2tgt.InstanceIdTarget(ec2Instance.instanceId, 443)
],
});
/* ---------- Listner ---------- */
alb.addListener(`listener-443`, {
port: 443,
defaultTargetGroups: [tg],
certificates: [
{
certificateArn: "arn:aws:iam::123456789012:server-certificate/ssl-cert-letsencrypt-domainname" // Certbot設定後、設定
}
],
});
/* ---------- R53 HostZone ---------- */
const hostzone = r53.HostedZone.fromLookup(this, `r53`, {
domainName: "example.com",
});
// /* ---------- R53 Record ---------- */
new r53.ARecord(this, `r53-record`, {
zone: hostzone,
recordName: `www.example.com`,
// target: r53.RecordTarget.fromIpAddresses(eip.attrPublicIp), // Certbot設定後はコメントアウト
target: r53.RecordTarget.fromAlias(new r53_tgt.LoadBalancerTarget(alb)),
});
}
}
これでエンドツーエンドでの