はじめに
HTTPのままでもアプリにアクセスすることはできますが、
この状態では通信が暗号化されておらず、安全とは言えません。
実運用では、HTTPS化(SSL/TLSの適用)は必須となります。
本記事では、AWS Certificate Manager(ACM)で証明書を発行し、
ALBに設定することで、https://app.example.com でアクセスできるようにします。
本シリーズの構成
1. VPC編
2. Route53 独自ドメイン編
3. ACM HTTPS化編(本記事)
4. WAFセキュリティ編
5. ECS Auto Scaling編
6. CI/CD編
今回のゴール
- 独自ドメインでHTTPSアクセスできるようにする
- HTTPアクセスをHTTPSに統一する
HTTPS化の構成
※ この図はHTTPS化の通信フローに絞った簡略図です。実際の本番構成では、ALBは複数AZのPublic Subnet、ECSは複数AZのPrivate Subnetに配置します。
前提条件
- Route53で独自ドメイン設定済み
- 前回までのCDKコード(Network / ECS / ALB / Service)が作成済み
- ALB経由でアプリへHTTPアクセスできる状態である
3-1. ACM証明書をCDKで作成する
ACM証明書は、アプリケーションとは独立したライフサイクルで管理するため、専用のスタック(CertificateStack)で管理します。
対象ファイル
lib/certificate-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
interface CertificateStackProps extends cdk.StackProps {
hostedZone: route53.IHostedZone;
}
export class CertificateStack extends cdk.Stack {
public readonly certificate: acm.Certificate;
constructor(scope: Construct, id: string, props: CertificateStackProps) {
super(scope, id, props);
this.certificate = new acm.Certificate(this, 'AppCertificate', {
domainName: 'app.example.com',
validation: acm.CertificateValidation.fromDns(props.hostedZone),
});
}
}
3-2. ALBの設定を更新する
ACMで作成した証明書を利用し、ALBでHTTPS通信を受け付けるように設定します。
あわせて、HTTPアクセスはHTTPSへリダイレクトするようにします。
対象ファイル
lib/alb-stack.ts
■ Propsの追加
証明書とホストゾーンを外部から受け取れるようにします。
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
interface AlbStackProps extends cdk.StackProps {
vpc: ec2.IVpc;
hostedZone: route53.IHostedZone;
certificate: acm.ICertificate;
}
■ HTTPS(443)を追加で許可する
既存のHTTP(80)に加えて、
HTTPS(443)へのアクセスを許可します。
this.albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS from Internet'
);
■ リスナー設定
HTTPはHTTPSへリダイレクトします。
HTTPSでは証明書を用いて通信を暗号化し、リクエストをECSへ転送します。
// HTTP → HTTPSリダイレクト
alb.addListener('HttpListener', {
port: 80,
defaultAction: elbv2.ListenerAction.redirect({
protocol: 'HTTPS',
port: '443',
permanent: true,
}),
});
// HTTPSリスナー
alb.addListener('HttpsListener', {
port: 443,
certificates: [props.certificate],
defaultTargetGroups: [this.targetGroup],
});
■ ホストゾーンの参照方法を変更する
ホストゾーンはbin側から受け取ったものを使用します。
new route53.ARecord(this, 'AppDomainRecord', {
zone: props.hostedZone,
recordName: 'app',
target: route53.RecordTarget.fromAlias(
new targets.LoadBalancerTarget(alb)
),
});
これにより、ALBはHTTPS公開に対応し、次の構成になります。
- HTTPS通信を受け付ける
- HTTPアクセスをHTTPSへ統一する
- 独自ドメインで安全にアクセスできる
3-3. CDKの構成を接続する
bin側でホストゾーンを一度だけ参照し、CertificateStackとAlbStackに渡します。
また、CertificateStackで作成した証明書をAlbStackへ渡して利用します。
これにより、ホストゾーンや証明書といった共通リソースを一元管理でき、
スタック間の依存関係が明確になります。
対象ファイル
bin/cdk-nextjs-infra.ts
※ 本シリーズでは CDKプロジェクト名を cdk-nextjs-infra としています。
import * as route53 from 'aws-cdk-lib/aws-route53';
import { CertificateStack } from '../lib/certificate-stack';
const hostedZone = route53.HostedZone.fromLookup(networkStack, 'HostedZone', {
domainName: 'example.com',
});
const certificateStack = new CertificateStack(app, 'NextjsInfraCertificateStack', {
env,
hostedZone,
});
new AlbStack(app, 'NextjsAlbStack', {
env,
vpc: networkStack.vpc,
hostedZone,
certificate: certificateStack.certificate,
});
const hostedZone = route53.HostedZone.fromLookup(networkStack, '...
HostedZone.fromLookup() は環境情報(account / region)をもとに解決されるため、env を持つStackスコープで実行するのが安全です。
そのため、ここではAppではなく既存のStack(networkStack)をスコープとして指定しています。
3-4. デプロイ
ServiceStackをデプロイします。
cdk deploy NextjsInfraServiceStack --profile <プロファイル名>
NextjsInfraServiceStack は他のスタックに依存しているため、
このコマンドで関連するスタックもあわせてデプロイされます。
本記事で作成したスタックに含まれるAWSリソースは、削除するまで料金が発生します。
検証が不要になった場合は、以下のコマンドでスタックを削除してください。
cdk destroy <スタック名> --profile <プロファイル名>
3-5. 動作確認
ブラウザで以下のURLへアクセスします。
https://app.example.com
確認ポイント
- アドレスバーに 「この接続は保護されています」 と表示される
- HTTPアクセスがHTTPSへリダイレクトされる
最終コード(今回追加・修正したファイル)
lib/certificate-stack.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
interface CertificateStackProps extends cdk.StackProps {
hostedZone: route53.IHostedZone;
}
export class CertificateStack extends cdk.Stack {
public readonly certificate: acm.Certificate;
constructor(scope: Construct, id: string, props: CertificateStackProps) {
super(scope, id, props);
this.certificate = new acm.Certificate(this, 'AppCertificate', {
domainName: 'app.example.com',
validation: acm.CertificateValidation.fromDns(props.hostedZone),
});
}
}
lib/alb-stack.ts
コード全体
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 route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
interface AlbStackProps extends cdk.StackProps {
vpc: ec2.IVpc;
hostedZone: route53.IHostedZone;
certificate: acm.ICertificate;
}
export class AlbStack extends cdk.Stack {
public readonly targetGroup: elbv2.ApplicationTargetGroup;
public readonly albSecurityGroup: ec2.SecurityGroup;
constructor(scope: Construct, id: string, props: AlbStackProps) {
super(scope, id, props);
this.albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
vpc: props.vpc,
allowAllOutbound: true,
description: 'Security group for public ALB',
});
this.albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP from Internet'
);
this.albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS from Internet'
);
const alb = new elbv2.ApplicationLoadBalancer(this, 'NextjsAlb', {
vpc: props.vpc,
internetFacing: true,
securityGroup: this.albSecurityGroup,
});
this.targetGroup = new elbv2.ApplicationTargetGroup(this, 'NextjsTargetGroup', {
vpc: props.vpc,
port: 3000,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.IP,
healthCheck: {
path: '/',
healthyHttpCodes: '200',
},
});
alb.addListener('HttpListener', {
port: 80,
defaultAction: elbv2.ListenerAction.redirect({
protocol: 'HTTPS',
port: '443',
permanent: true,
}),
});
alb.addListener('HttpsListener', {
port: 443,
certificates: [props.certificate],
defaultTargetGroups: [this.targetGroup],
});
new route53.ARecord(this, 'AppDomainRecord', {
zone: props.hostedZone,
recordName: 'app',
target: route53.RecordTarget.fromAlias(
new targets.LoadBalancerTarget(alb)
),
});
new cdk.CfnOutput(this, 'AlbDnsName', {
value: alb.loadBalancerDnsName,
});
}
}
bin/cdk-nextjs-infra.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { EcrStack } from '../lib/ecr-stack';
import { NetworkStack } from '../lib/network-stack';
import { EcsStack } from '../lib/ecs-stack';
import { CertificateStack } from '../lib/certificate-stack';
import { AlbStack } from '../lib/alb-stack';
import { ServiceStack } from '../lib/service-stack';
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
new EcrStack(app, 'NextjsInfraEcrStack', {
env,
});
const networkStack = new NetworkStack(app, 'NextjsInfraNetworkStack', {
env,
});
const ecsStack = new EcsStack(app, 'NextjsEcsStack', {
env,
vpc: networkStack.vpc,
});
const hostedZone = route53.HostedZone.fromLookup(networkStack, 'HostedZone', {
domainName: 'example.com', // 自分のドメインに置き換えてください
});
const certificateStack = new CertificateStack(app, 'NextjsInfraCertificateStack', {
env,
hostedZone,
});
const albStack = new AlbStack(app, 'NextjsInfraAlbStack', {
env,
vpc: networkStack.vpc,
hostedZone,
certificate: certificateStack.certificate,
});
new ServiceStack(app, 'NextjsInfraServiceStack', {
env,
cluster: ecsStack.cluster,
taskDefinition: ecsStack.taskDefinition,
targetGroup: albStack.targetGroup,
vpc: networkStack.vpc,
albSecurityGroup: albStack.albSecurityGroup,
});
ここまでの成果
これにより、ALBはHTTPS(443)でリクエストを受け付けられるようになり、
HTTP(80)へのアクセスもHTTPSへ自動リダイレクトされるようになりました。
その結果、独自ドメインで暗号化通信を行える構成が整いました。
次回
次回は「WAFをALBに適用してセキュリティを強化」します。