[CDK] 内部 ALB に AWS Certificate Manager でサーバ証明書を発行してみた
製造ビジネステクノロジー部の新澤です。
インターネットに公開しないプライベート VPC 内で ELB や API Gateway のプライベート REST API を利用する際、 HTTPS で通信するために独自ドメインで証明書を発行したいというユースケースで、「プライベート CA を作るのは運用の手間や利用費が気になる」といった場合に選択できる方法として、 Route53 でパブリックホストゾーンとプライベートホストゾーンで同じドメイン名を使用するスプリットビュー DNS を使う方法があります。
今回は、スプリットビュー DNS 構成を用いた証明書の発行を AWS CDK で実装してみます。
※ スプリットビュー DNS に関しては以下が詳しいです。
【Route53】スプリットビューDNSの名前解決順序を整理してみた | DevelopersIO
構成図
今回の構成では、下図のようにプライベートサブネットに配置された 内部ALB に対して、独自ドメインで HTTPS 通信を行えるようにします。
なお、前提条件として、パブリックホストゾーンは既に存在しているものを利用する想定としています。
実装してみる
プライベートホストゾーン
パブリックホストゾーンのサブドメインとなるようにプライベートホストゾーンを作成します。(パブリックホストゾーンと同一ドメイン名でも機能しますが、わかりやすくするためサブドメインとしています)
パブリックホストゾーンは、ACM でサーバ証明書発行する時の DNS 検証を行う際に CNAME レコードを登録するために利用します。
また、プライベートホストゾーンは、 ALB のエイリアスレコードを登録するために利用します。
const publicHostedZone = route53.HostedZone.fromLookup(this, 'PublicHostedZone', {
domainName: props.domainName,
privateZone: false,
});
const privateHostedZone = new route53.PrivateHostedZone(this, 'PrivateHostedZone', {
vpc,
zoneName: `private.${props.domainName}`,
});
サーバ証明書の発行
AWS Certificate Managerでサーバ証明書を発行します。
パブリックホストゾーンで DNS 検証を行うように設定します。
const certificate = new acm.Certificate(this, 'Certificate', {
domainName: `api.${privateHostedZone.zoneName}`,
validation: acm.CertificateValidation.fromDns(publicHostedZone),
});
エイリアスレコード
プライベートホストゾーンに Fargate サービスの ALB に対するエイリアスレコードを登録します。
new route53.ARecord(this, 'AliasRecord', {
zone: privateHostedZone,
recordName: `api.${privateHostedZone.zoneName}`,
target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(service.loadBalancer)),
});
CDK コード全体
以下が作成したスタック全体のコードになります。
なお、ApplicationLoadBalancedFargateService
だけでプライベートホストゾーンへのエイリアスレコードの登録まで可能なのですが、今回は説明のためあえてARecord
を分けて実装しています。
コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
aws_route53 as route53,
aws_route53_targets as route53targets,
aws_certificatemanager as acm, aws_ec2 as ec2,
aws_elasticloadbalancingv2 as elbv2,
aws_ecs as ecs,
aws_ecs_patterns as ecsPatterns
} from 'aws-cdk-lib';
export interface InternalAlbCertificateStackProps extends cdk.StackProps {
readonly domainName: string;
}
export class InternalAlbCertificateStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: InternalAlbCertificateStackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
]
});
const publicHostedZone = route53.HostedZone.fromLookup(this, 'PublicHostedZone', {
domainName: props.domainName,
privateZone: false,
});
const privateHostedZone = new route53.PrivateHostedZone(this, 'PrivateHostedZone', {
vpc,
zoneName: `private.${props.domainName}`,
});
const certificate = new acm.Certificate(this, 'Certificate', {
domainName: `api.${privateHostedZone.zoneName}`,
validation: acm.CertificateValidation.fromDns(publicHostedZone),
});
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'SampleService', {
vpc: vpc,
cpu: 256,
memoryLimitMiB: 512,
desiredCount: 1,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/nginx/nginx:1.27'),
containerPort: 80,
},
publicLoadBalancer: false,
protocol: elbv2.ApplicationProtocol.HTTPS,
certificate: certificate,
loadBalancerName: 'SampleLoadBalancer',
// ↓ 以下でもエイリアスレコードを登録可能
// domainName: `api.${privateHostedZone.zoneName}`,
// domainZone: privateHostedZone,
});
new route53.ARecord(this, 'AliasRecord', {
zone: privateHostedZone,
recordName: `api.${privateHostedZone.zoneName}`,
target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(service.loadBalancer)),
});
const bastion = new ec2.BastionHostLinux(this, 'BastionHost', {
vpc,
instanceName: 'BastionHost',
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
machineImage: ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
subnetSelection: {
subnetType: ec2.SubnetType.PUBLIC,
},
requireImdsv2: true,
});
service.service.connections.allowFrom(bastion, ec2.Port.HTTPS, 'Allow HTTP traffic from Bastion Host');
}
}
デプロイして動作確認
デプロイ後にBastion Hostに接続して、 ALB にリクエストしてみます。
[ssm-user@ip-10-0-31-139 bin]$ curl https://api.private.${ドメイン名}/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
内部 ALB に、独自ドメインで HTTPS リクエストできました!
おわりに
今回のようなユースケースの場合、AWS Private CA の利用も選択肢になりますが、プライベート CA 当たり月額400 USD が発生するため、採用に二の足を踏む場面もあるのではないでしょうか?(私はあります)
今回のようにスプリットビュー DNS を利用することで、利用費を抑えることが可能になります。
また、今回ご紹介した ALB だけでなく API Gateway のプライベート REST API でも利用可能ですので、活用の幅も広いかと思います。
どなたかのお役に立てれば幸いです。