CDKでlaravel12をECS Fargateで動作させる
前回まででECRにイメージが作成できているかなと思うんだけど、ある程度問題ありありのイメージである。
-
SQLite
(migrate済)が含まれている - .envを内包している
- ローカルストレージの事を何も考えてない(SQLiteにも通じる)
- しかし動作はする
というとんでもないイメージであるが、ただし「動く」はず。動くならこれをベースにCDK
でとりあえず動作させてみようということだ。動作させてみないと本当に動くのかどうなのか全くわからんからね。
ECRリポジトリがキマってる場合は一度削除しておこう。これもCDK
で作りたいので。
my-php-fpm
を削除。同様にmy-nginx
も削除しておく
Bootstrap済みCDK環境を手に入れる
まあこれはAdministratorAccess
権限のcloudshellでcdk bootstrapするだけ
cdk initする
ここからいよいよ作成フェーズだ。通常typescript
が使われると思うのでそのようにする。
mkdir laravel-fargate-demo
cd laravel-fargate-demo
cdk init app --language=typescript
最初のstackは消していいんじゃないかな。
git rm --force lib/laravel-fargate-demo-stack.ts
generate only
的なオプションを付けないと自動的にgit管理になるので、git rm
してるけどrm
でもよい。なおディレクトリが空になるとgit
はディレクトリも消してしまう。まあいいよねこの辺は。
vpc作る
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
export class VpcStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'LaravelDemoVpc', {
maxAzs: 2, // ap-northeast-1a / 1c など
subnetConfiguration: [
{
cidrMask: 24,
name: 'PublicSubnet',
subnetType: ec2.SubnetType.PUBLIC,
},
],
natGateways: 0, // 最小構成:NAT不要
});
new cdk.CfnOutput(this, 'VpcId', {
value: this.vpc.vpcId,
});
}
}
ここでbin/laravel-fargate-demo.tsがエントリポイントになる。ここで最初にあるエントリーだが
new LaravelFargateDemoStack(app, 'LaravelFargateDemoStack', {
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */
/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
これは削除してよいエントリーなのだが、ここでenv
とかのpropsを渡さないとコンテキストルックアップが使えなくなったりして微妙なんで、ここでは渡しておく。コンテキストルックアップはデフォルトVPCを使う場合なんかでよく出てくるけど、ここではSSLのドメインを参照するときとかに使うので、一応env
をちょいちょい渡していくことにしよう。
というわけで以下のようにする。
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
const vpcStack = new VpcStack(app, 'DemoVpcStack', { env });
そしたら
cdk diff
とかして
Resources
[+] AWS::EC2::VPC LaravelDemoVpc LaravelDemoVpc6FD17F1E
[+] AWS::EC2::Subnet LaravelDemoVpc/PublicSubnetSubnet1/Subnet LaravelDemoVpcPublicSubnetSubnet1SubnetCE8D5AEC
[+] AWS::EC2::RouteTable LaravelDemoVpc/PublicSubnetSubnet1/RouteTable LaravelDemoVpcPublicSubnetSubnet1RouteTable0F0614E9
[+] AWS::EC2::SubnetRouteTableAssociation LaravelDemoVpc/PublicSubnetSubnet1/RouteTableAssociation LaravelDemoVpcPublicSubnetSubnet1RouteTableAssociation8DB612CD
[+] AWS::EC2::Route LaravelDemoVpc/PublicSubnetSubnet1/DefaultRoute LaravelDemoVpcPublicSubnetSubnet1DefaultRoute472BB980
[+] AWS::EC2::Subnet LaravelDemoVpc/PublicSubnetSubnet2/Subnet LaravelDemoVpcPublicSubnetSubnet2Subnet1D476FC2
[+] AWS::EC2::RouteTable LaravelDemoVpc/PublicSubnetSubnet2/RouteTable LaravelDemoVpcPublicSubnetSubnet2RouteTable5932B141
[+] AWS::EC2::SubnetRouteTableAssociation LaravelDemoVpc/PublicSubnetSubnet2/RouteTableAssociation LaravelDemoVpcPublicSubnetSubnet2RouteTableAssociationF4F57D3C
[+] AWS::EC2::Route LaravelDemoVpc/PublicSubnetSubnet2/DefaultRoute LaravelDemoVpcPublicSubnetSubnet2DefaultRoute961B1048
[+] AWS::EC2::InternetGateway LaravelDemoVpc/IGW LaravelDemoVpcIGW0101CC45
[+] AWS::EC2::VPCGatewayAttachment LaravelDemoVpc/VPCGW LaravelDemoVpcVPCGWBCCF278A
[+] Custom::VpcRestrictDefaultSG LaravelDemoVpc/RestrictDefaultSecurityGroupCustomResource LaravelDemoVpcRestrictDefaultSecurityGroupCustomResource8E88B0C0
[+] AWS::IAM::Role Custom::VpcRestrictDefaultSGCustomResourceProvider/Role CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0
[+] AWS::Lambda::Function Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E
Outputs
[+] Output VpcId VpcId: {"Value":{"Ref":"LaravelDemoVpc6FD17F1E"}}
できるものを確認したら
cdk deploy
deployの確認画面
これでVPCが一本できる
VPCが作成された。リソースマップも一応確認しておくこと
ECR
冒頭でECRリポジトリーは削除してしまったので、ここでCDKを用いてECRリポジトリーを作成してみよう。以下のようにしてmyapp-nginx
とmyapp-php-fpm
リポジトリーの作成を行うスクリプトを起こす。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
export class EcrStack extends cdk.Stack {
public readonly nginxRepo: ecr.Repository;
public readonly phpFpmRepo: ecr.Repository;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// myapp-nginx リポジトリ
this.nginxRepo = new ecr.Repository(this, 'MyAppNginxRepo', {
repositoryName: 'myapp-nginx',
imageScanOnPush: true,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// myapp-php-fpm リポジトリ
this.phpFpmRepo = new ecr.Repository(this, 'MyAppPhpFpmRepo', {
repositoryName: 'myapp-php-fpm',
imageScanOnPush: true,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
new cdk.CfnOutput(this, 'NginxRepoUri', {
value: this.nginxRepo.repositoryUri,
});
new cdk.CfnOutput(this, 'PhpFpmRepoUri', {
value: this.phpFpmRepo.repositoryUri,
});
}
}
さらにエントリポイントを更新してこれを読むように
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
+import { EcrStack } from '../lib/ecr-stack';
const app = new cdk.App();
const env = {
@@ -9,3 +10,4 @@ const env = {
};
const vpcStack = new VpcStack(app, 'DemoVpcStack', { env });
+const ecrStack = new EcrStack(app, 'DemoEcrStack', { env });
その後cdk diff
で
Resources
[+] AWS::ECR::Repository MyAppNginxRepo MyAppNginxRepoDAA7AD17
[+] AWS::ECR::Repository MyAppPhpFpmRepo MyAppPhpFpmRepo398BB7C4
のように作成リソースを確認したら
cdk deploy DemoEcrStack
するとリポジトリーができる
リポジトリーができた
RETAINについての解説と再生成について
ここで
removalPolicy: cdk.RemovalPolicy.RETAIN,
とかの記述が見られるがretainとは日本語で「保持」という意味で、ここからわかるように削除したとき残すという指示になる。
たとえば
laravel-fargate-demo $ cdk destroy DemoEcrStack
Are you sure you want to delete: DemoEcrStack (y/n)? y
DemoEcrStack: destroying... [1/1]
✅ DemoEcrStack: destroyed
こんな感じでdestroyしても残っている。awscli
で確認した例
aws ecr describe-repositories
...
{
"repositoryArn": "arn:aws:ecr:ap-northeast-1:****:repository/myapp-nginx",
...
さらにこの状態で再度デプロイすると
cdk deploy DemoEcrStack
moEcrStack failed: _ToolkitError: The stack named DemoEcrStack failed creation, it may need to be manually deleted from the AWS console:
こんな感じになるので、手動で消すか、あるいは既存のものを使う場合は、そもそもCDKで作成するのではなく
const nginxRepo = ecr.Repository.fromRepositoryName(this, 'NginxRepo', 'myapp-nginx');
const phpFpmRepo = ecr.Repository.fromRepositoryName(this, 'PhpFpmRepo', 'myapp-php-fpm');
こういう形で整える必要がある。手動作成あるいは共通インフラを別stackに出すなどしてここでのStackはfromRepositoryName
で頑張るというのがむしろいいかも。ここではCDKで作成してみたけど、その辺は運用考えてみてください。
序でだからECRプッシュのユーザーも作ろう
というわけでRETAIN
が入ってるので既にdeployしてしまった場合は一度webから消してやりなおすとして、このリポジトリを触れるIAMユーザーを作成しキーを出す。CI/CD
する場合はOICD
も考えてくださいねってことなんだけどまあそれはいいわ。
(参考)
lib/ecr-stack.ts
@@ -1,6 +1,7 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
+import * as iam from 'aws-cdk-lib/aws-iam';
export class EcrStack extends cdk.Stack {
public readonly nginxRepo: ecr.Repository;
@@ -30,5 +31,48 @@ export class EcrStack extends cdk.Stack {
new cdk.CfnOutput(this, 'PhpFpmRepoUri', {
value: this.phpFpmRepo.repositoryUri,
});
+
+ // IAMユーザー作成
+ const ecrUser = new iam.User(this, 'EcrPushUser', {
+ userName: 'ecr-push-user',
+ });
+
+ // ポリシーアタッチ(両リポジトリへのPush権限)
+ const ecrPolicy = new iam.Policy(this, 'EcrPushPolicy', {
+ statements: [
+ new iam.PolicyStatement({
+ actions: ['ecr:GetAuthorizationToken'],
+ resources: ['*'],
+ }),
+ new iam.PolicyStatement({
+ actions: [
+ 'ecr:BatchCheckLayerAvailability',
+ 'ecr:PutImage',
+ 'ecr:InitiateLayerUpload',
+ 'ecr:UploadLayerPart',
+ 'ecr:CompleteLayerUpload',
+ ],
+ resources: [
+ this.nginxRepo.repositoryArn,
+ this.phpFpmRepo.repositoryArn,
+ ],
+ }),
+ ],
+ });
+ ecrPolicy.attachToUser(ecrUser);
+
+ // アクセスキー作成
+ const accessKey = new iam.CfnAccessKey(this, 'EcrPushUserAccessKey', {
+ userName: ecrUser.userName,
+ });
+
+ new cdk.CfnOutput(this, 'EcrPushAccessKeyId', {
+ value: accessKey.ref,
+ });
+
+ new cdk.CfnOutput(this, 'EcrPushSecretAccessKey', {
+ value: accessKey.attrSecretAccessKey,
+ description: '!! Copy this immediately. It will not be shown again.',
+ });
}
}
cdk diff
すると
Resources
[+] AWS::IAM::User EcrPushUser EcrPushUserA1CB08D3
[+] AWS::IAM::Policy EcrPushPolicy EcrPushPolicy42DA89BF
[+] AWS::IAM::AccessKey EcrPushUserAccessKey EcrPushUserAccessKey
Outputs
[+] Output EcrPushAccessKeyId EcrPushAccessKeyId: {"Value":{"Ref":"EcrPushUserAccessKey"}}
[+] Output EcrPushSecretAccessKey EcrPushSecretAccessKey: {"Description":"!! Copy this immediately. It will not be shown again.","Value":{"Fn::GetAtt":["EcrPushUserAccessKey","SecretAccessKey"]}}
こんな感じになるんで、deployしといてください
cdk deploy DemoEcrStack
こんな感じで鍵が取れる
鍵の出力、本来はここでもprintしない方がいいかもしれないが
docker imageのpush
ここは冒頭で述べたように割愛
(再掲)
ECSクラスターとタスク定義の作成
ではやっていく、ここは1つのスタックにつめこむ事にする。もし1クラスターに複数のサービスを与える構想なら1つのスタックにしない方がいいかも。ここではlib/ecs-stack.tsとする
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
interface EcsStackProps extends cdk.StackProps {
vpc: ec2.Vpc;
}
export class EcsStack extends cdk.Stack {
public readonly cluster: ecs.Cluster;
public readonly taskDefinition: ecs.FargateTaskDefinition;
constructor(scope: Construct, id: string, props: EcsStackProps) {
super(scope, id, props);
// ECSクラスター作成
this.cluster = new ecs.Cluster(this, 'LaravelDemoCluster', {
vpc: props.vpc,
containerInsights: true,
});
// Fargateタスク定義
this.taskDefinition = new ecs.FargateTaskDefinition(this, 'LaravelFargateTaskDef', {
cpu: 256,
memoryLimitMiB: 512,
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64, // !!!!!!!!!!!!!! ARM64指定
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
},
});
// IAMロール(タスク実行用)に必要なポリシーはCDKが自動付与
// ロググループ(CloudWatch)
const logGroup = new logs.LogGroup(this, 'LaravelLogGroup', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Nginxコンテナ
this.taskDefinition.addContainer('NginxContainer', {
image: ecs.ContainerImage.fromEcrRepository(
ecr.Repository.fromRepositoryName(this, 'NginxRepo', 'myapp-nginx')
),
containerName: 'nginx',
portMappings: [{ containerPort: 80 }],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nginx',
logGroup,
}),
});
// PHP-FPMコンテナ
this.taskDefinition.addContainer('PhpFpmContainer', {
image: ecs.ContainerImage.fromEcrRepository(
ecr.Repository.fromRepositoryName(this, 'PhpFpmRepo', 'myapp-php-fpm')
),
containerName: 'php-fpm',
portMappings: [{ containerPort: 9000 }],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'php-fpm',
logGroup,
}),
});
new cdk.CfnOutput(this, 'ClusterName', {
value: this.cluster.clusterName,
});
new cdk.CfnOutput(this, 'TaskDefinitionArn', {
value: this.taskDefinition.taskDefinitionArn,
});
}
}
cpu/memory共に最底辺を指定
さらにエントリーポイントを更新する
@@ -1,6 +1,8 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
+import { EcrStack } from '../lib/ecr-stack';
+import { EcsStack } from '../lib/ecs-stack';
const app = new cdk.App();
const env = {
@@ -9,3 +11,8 @@ const env = {
};
const vpcStack = new VpcStack(app, 'DemoVpcStack', { env });
+const ecrStack = new EcrStack(app, 'DemoEcrStack', { env });
+const ecsStack = new EcsStack(app, 'DemoEcsStack', {
+ env,
+ vpc: vpcStack.vpc,
+});
cdk diff
すると
Resources
[+] AWS::ECS::Cluster LaravelDemoCluster LaravelDemoCluster39A13F47
[+] AWS::IAM::Role LaravelFargateTaskDef/TaskRole LaravelFargateTaskDefTaskRole6DDE0619
[+] AWS::ECS::TaskDefinition LaravelFargateTaskDef LaravelFargateTaskDef9C57D66D
[+] AWS::IAM::Role LaravelFargateTaskDef/ExecutionRole LaravelFargateTaskDefExecutionRole5B112D0A
[+] AWS::IAM::Policy LaravelFargateTaskDef/ExecutionRole/DefaultPolicy LaravelFargateTaskDefExecutionRoleDefaultPolicy3E6EE2B2
[+] AWS::Logs::LogGroup LaravelLogGroup LaravelLogGroupC512EA17
Outputs
[+] Output ClusterName ClusterName: {"Value":{"Ref":"LaravelDemoCluster39A13F47"}}
[+] Output TaskDefinitionArn TaskDefinitionArn: {"Value":{"Ref":"LaravelFargateTaskDef9C57D66D"}}
cdk deploy DemoEcsStack
した後作成されたリソースをwebで確認。https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters?region=ap-northeast-1 より
クラスターが出来ている
さらに「タスク定義」に移動する
ARM64
でタスク定義ができている
サービス作る
あとはこのタスクを動かし続けるサービスを作れば基本的にはlaravelトップページの御尊顔を拝めるはずだ。
@@ -66,6 +66,26 @@ export class EcsStack extends cdk.Stack {
}),
});
+ // SecurityGroup
+ const serviceSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSG', {
+ vpc: props.vpc,
+ allowAllOutbound: true,
+ description: 'Security group for Fargate service',
+ });
+ serviceSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP'); // ポート80フルオープン
+
+ // Fargate サービス
+ const fargateService = new ecs.FargateService(this, 'LaravelFargateService', {
+ cluster: this.cluster,
+ taskDefinition: this.taskDefinition,
+ desiredCount: 1,
+ assignPublicIp: true,
+ securityGroups: [serviceSecurityGroup],
+ vpcSubnets: {
+ subnetType: ec2.SubnetType.PUBLIC,
+ },
+ });
+
new cdk.CfnOutput(this, 'ClusterName', {
value: this.cluster.clusterName,
});
@@ -73,5 +93,9 @@ export class EcsStack extends cdk.Stack {
new cdk.CfnOutput(this, 'TaskDefinitionArn', {
value: this.taskDefinition.taskDefinitionArn,
});
+
+ new cdk.CfnOutput(this, 'ServiceName', {
+ value: fargateService.serviceName,
+ });
}
}
cdk deploy DemoEcsStack
deployすると「結構時間かかった後」サービスが作成され、タスクが起動してくるはず(うまくいけば)。うまくいかなければcloudwatchとかのログとかみてください。
タスクが起動した
タスクのネットワーキングのタブからパブリックIPを得る
この辺からネットワークを確認してアクセスするとlaravelの画面が出る
laravel12のstarter kit画面
起動確認としては一応ログイン成功まで確認しとくこと
ログインできている
ALBにする
まさか個別タスクのIPを指定してアクセスさせるわけにはいかんのでALBで包む。ALBは結構高いんでテストで構築するのはまだしも、そのまま運用せずほったらかしにするとかなり痛いから注意が必要
ALBの設定においてはCDK の高レベル construct を使い、ALB + TargetGroup + Listener + SecurityGroup を全部自動生成するのがよい。字面はむずいがライブラリーを置き換えるコードをはめこむだけなのでdiffでみれば割とsimpleではないだろうか。
@@ -5,6 +5,7 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
+import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
interface EcsStackProps extends cdk.StackProps {
vpc: ec2.Vpc;
@@ -75,15 +76,16 @@ export class EcsStack extends cdk.Stack {
serviceSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');
// Fargate サービス
- const fargateService = new ecs.FargateService(this, 'LaravelFargateService', {
+ const albFargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LaravelAlbFargateService', {
cluster: this.cluster,
taskDefinition: this.taskDefinition,
desiredCount: 1,
assignPublicIp: true,
- securityGroups: [serviceSecurityGroup],
- vpcSubnets: {
- subnetType: ec2.SubnetType.PUBLIC,
- },
+ publicLoadBalancer: true,
+ listenerPort: 80,
+ });
+ new cdk.CfnOutput(this, 'AlbDnsName', {
+ value: albFargateService.loadBalancer.loadBalancerDnsName,
});
new cdk.CfnOutput(this, 'ClusterName', {
@@ -95,7 +97,7 @@ export class EcsStack extends cdk.Stack {
});
new cdk.CfnOutput(this, 'ServiceName', {
- value: fargateService.serviceName,
+ value: albFargateService.service.serviceName,
});
}
}
これで
cdk diff
にて
Resources
[-] AWS::ECS::Service LaravelFargateService/Service LaravelFargateService6560B798 destroy
[+] AWS::ElasticLoadBalancingV2::LoadBalancer LaravelAlbFargateService/LB LaravelAlbFargateServiceLBB23286CC
[+] AWS::EC2::SecurityGroup LaravelAlbFargateService/LB/SecurityGroup LaravelAlbFargateServiceLBSecurityGroup85184954
[+] AWS::EC2::SecurityGroupEgress LaravelAlbFargateService/LB/SecurityGroup/to DemoEcsStackLaravelAlbFargateServiceSecurityGroup81C68626:80 LaravelAlbFargateServiceLBSecurityGrouptoDemoEcsStackLaravelAlbFargateServiceSecurityGroup81C6862680C3164CDA
[+] AWS::ElasticLoadBalancingV2::Listener LaravelAlbFargateService/LB/PublicListener LaravelAlbFargateServiceLBPublicListener0DD9EEB6
[+] AWS::ElasticLoadBalancingV2::TargetGroup LaravelAlbFargateService/LB/PublicListener/ECSGroup LaravelAlbFargateServiceLBPublicListenerECSGroup99A93CC7
[+] AWS::ECS::Service LaravelAlbFargateService/Service/Service LaravelAlbFargateServiceA0372A8E
[+] AWS::EC2::SecurityGroup LaravelAlbFargateService/Service/SecurityGroup LaravelAlbFargateServiceSecurityGroup031E26FB
[+] AWS::EC2::SecurityGroupIngress LaravelAlbFargateService/Service/SecurityGroup/from DemoEcsStackLaravelAlbFargateServiceLBSecurityGroup96CD2788:80 LaravelAlbFargateServiceSecurityGroupfromDemoEcsStackLaravelAlbFargateServiceLBSecurityGroup96CD278880C6FB95ED
Outputs
[+] Output LaravelAlbFargateService/LoadBalancerDNS LaravelAlbFargateServiceLoadBalancerDNSCC803231: {"Value":{"Fn::GetAtt":["LaravelAlbFargateServiceLBB23286CC","DNSName"]}}
[+] Output LaravelAlbFargateService/ServiceURL LaravelAlbFargateServiceServiceURL54CACF1E: {"Value":{"Fn::Join":["",["http://",{"Fn::GetAtt":["LaravelAlbFargateServiceLBB23286CC","DNSName"]}]]}}
[+] Output AlbDnsName AlbDnsName: {"Value":{"Fn::GetAtt":["LaravelAlbFargateServiceLBB23286CC","DNSName"]}}
[~] Output ServiceName ServiceName: {"Value":{"Fn::GetAtt":["LaravelFargateService6560B798","Name"]}} to {"Value":{"Fn::GetAtt":["LaravelAlbFargateServiceA0372A8E","Name"]}}
diffを確認したら
cdk deploy DemoEcsStack
やってることがやってることだけに非常にapplyが長くなるが、ちょっと待ってると
AlbDnsName
が出力されている
こんな感じでAlbDnsName
が出てくるので、これにシンプルにアクセスすると...
ALB経由でトップページにアクセス
こうなるわけだ。まあこれは
サービスの詳細 → ロードバランサーの状態
のところにあるこれを押して
ロードバランサーの設定からDNS名を確認する方法
この手法でも取れます
SSL
ここまでで9割終わってんだけどSSL proxyしたときちゃんとlaravelの画面出ねえってのが割とあるんで、やっときましょう。これは基本的にイメージビルドの状態でちゃんと設定しておかないといけなくて
参考
この設定しておかないとまずうまくいかない。起動してまっしろになるとかある。
ドメインを用意
SSLの構築にはこれはドメインが必要。ここではtest.catatsumuri.org
ってのを持ってきた。この辺は各自なんとか調達してroute53のパブリックホストゾーンに放りこんでください。route53に放りこめないって?そりゃ調査するしかねえな...
パブリックホストゾーンに当該ドメインをセット
@@ -6,6 +6,9 @@ import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
+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 EcsStackProps extends cdk.StackProps {
vpc: ec2.Vpc;
@@ -75,6 +78,16 @@ export class EcsStack extends cdk.Stack {
});
serviceSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');
+ const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
+ domainName: 'test.catatsumuri.org',
+ });
+
+ const certificate = new acm.DnsValidatedCertificate(this, 'LaravelCert', {
+ domainName: 'laravel.test.catatsumuri.org',
+ hostedZone,
+ region: 'ap-northeast-1',
+ });
+
// Fargate サービス
const albFargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LaravelAlbFargateService', {
cluster: this.cluster,
@@ -82,7 +95,10 @@ export class EcsStack extends cdk.Stack {
desiredCount: 1,
assignPublicIp: true,
publicLoadBalancer: true,
- listenerPort: 80,
+ domainName: 'laravel.test.catatsumuri.org',
+ domainZone: hostedZone,
+ certificate,
+ listenerPort: 443,
});
new cdk.CfnOutput(this, 'AlbDnsName', {
value: albFargateService.loadBalancer.loadBalancerDnsName,
そしたら
cdk diff
すると
Resources
[+] AWS::IAM::Role LaravelCert/CertificateRequestorFunction/ServiceRole LaravelCertCertificateRequestorFunctionServiceRoleB39F39E4
[+] AWS::IAM::Policy LaravelCert/CertificateRequestorFunction/ServiceRole/DefaultPolicy LaravelCertCertificateRequestorFunctionServiceRoleDefaultPolicy96CF41EA
[+] AWS::Lambda::Function LaravelCert/CertificateRequestorFunction LaravelCertCertificateRequestorFunction6AEED3FA
[+] AWS::Logs::LogGroup LaravelCert/CertificateRequestorFunction/LogGroup LaravelCertCertificateRequestorFunctionLogGroupDF0C1241
[+] AWS::CloudFormation::CustomResource LaravelCert/CertificateRequestorResource LaravelCertCertificateRequestorResource944F34FF
[+] AWS::Route53::RecordSet LaravelAlbFargateService/DNS LaravelAlbFargateServiceDNSB00FE099
[~] AWS::EC2::SecurityGroup LaravelAlbFargateService/LB/SecurityGroup LaravelAlbFargateServiceLBSecurityGroup85184954
└─ [~] SecurityGroupIngress
└─ @@ -1,9 +1,9 @@
[ ] [
[ ] {
[ ] "CidrIp": "0.0.0.0/0",
[-] "Description": "Allow from anyone on port 80",
[-] "FromPort": 80,
[+] "Description": "Allow from anyone on port 443",
[+] "FromPort": 443,
[ ] "IpProtocol": "tcp",
[-] "ToPort": 80
[+] "ToPort": 443
[ ] }
[ ] ]
[~] AWS::ElasticLoadBalancingV2::Listener LaravelAlbFargateService/LB/PublicListener LaravelAlbFargateServiceLBPublicListener0DD9EEB6
├─ [+] Certificates
│ └─ [{"CertificateArn":{"Fn::GetAtt":["LaravelCertCertificateRequestorResource944F34FF","Arn"]}}]
├─ [~] Port
│ ├─ [-] 80
│ └─ [+] 443
└─ [~] Protocol
├─ [-] HTTP
└─ [+] HTTPS
みたいにhttpsに切り変わる風味になる。route53でゾーンを管理してればあとは証明書の発行から適当にやってくれる
証明書がセットされSSLが有効となった
ここまでで完成。
壊す
壊すテストは必ずすること。特にテストで作って起動しっぱなしにしてると問答無用で課金(先述したけど、この構成では特にALBがきつい)
cdk destroy --all
削除中はcloudformationのスタック一覧見ておくと精神的に楽かも
消されている状況がより明確にわかる気がする
再構築
cdk deploy --all
でやっぱりECRリポジトリーがretainされてて辛いって場合
先述したようにECRがある状態でCREATEするとエラーになるんで
Already Exists的なエラー
@@ -4,26 +4,15 @@ import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as iam from 'aws-cdk-lib/aws-iam';
export class EcrStack extends cdk.Stack {
- public readonly nginxRepo: ecr.Repository;
- public readonly phpFpmRepo: ecr.Repository;
+ public readonly nginxRepo: ecr.IRepository;
+ public readonly phpFpmRepo: ecr.IRepository;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
- // myapp-nginx リポジトリ
- this.nginxRepo = new ecr.Repository(this, 'MyAppNginxRepo', {
- repositoryName: 'myapp-nginx',
- imageScanOnPush: true,
- removalPolicy: cdk.RemovalPolicy.RETAIN,
- });
-
- // myapp-php-fpm リポジトリ
- this.phpFpmRepo = new ecr.Repository(this, 'MyAppPhpFpmRepo', {
- repositoryName: 'myapp-php-fpm',
- imageScanOnPush: true,
- removalPolicy: cdk.RemovalPolicy.RETAIN,
- });
-
+ // 既存のリポジトリを参照
+ this.nginxRepo = ecr.Repository.fromRepositoryName(this, 'MyAppNginxRepo', 'myapp-nginx');
+ this.phpFpmRepo = ecr.Repository.fromRepositoryName(this, 'MyAppPhpFpmRepo', 'myapp-php-fpm');
こんな感じで
IAMも消したくない場合もあるかも
先にIAMユーザーを何かしら作っておいて(ここではecr-push-user
)
const ecrUser = iam.User.fromUserName(this, 'ExistingEcrPushUser', 'ecr-push-user');
とかで参照するようにする、とか。
必要な知識がこの程度でも割と多岐に渡る
項目 | 説明 |
---|---|
cdk init app --language=typescript |
CDKアプリケーションの初期化 |
Stack分割(VpcStack, EcrStack, EcsStack) | スタックごとの責務分離 |
RemovalPolicy.RETAIN |
リソース削除と再デプロイの挙動理解 |
fromRepositoryName で既存ECRを参照 |
手動管理リソースのCDK参照方法 |
ApplicationLoadBalancedFargateService |
ECS + ALBの高レベルConstruct |
Fargate タスク定義 (cpu , memory ) |
起動条件の最低指定 |
タスク → サービス → クラスター | ECSのリソース構造 |
DockerイメージのECR push手順 | aws cli + docker login の基本操作 |
IAMユーザーへのPush権限付与 | ユーザーへの適切な権限管理 |
php-fpm + nginx の2コンテナ構成 |
イメージ分割と通信連携の想定 |
Laravel Starter Kit が動く程度の構成 | DBアクセス/HTTPSを要求しない構成に制限 |
.env 、SQLite込み |
運用には程遠いがPoC 的な意味ではまぁ... |
ACM証明書の DnsValidatedCertificate
|
自動でACM発行 & Route53レコード生成 |
domainZone , domainName
|
CDKによるALB+HTTPS自動化構成の設定値 |
Public Subnet / NAT不要構成 | 検証環境としての最小構成選択の意図 |
allowAllOutbound SG |
ECSサービスからの外部アクセス許可の必要性 |
cdk destroy --all の重要性 |
放置によるコスト爆発の防止 |
ALBの料金構造に対する警戒 | 少人数検証でも本番並みに金がかかる現実 |
CloudFormation Stackの確認 | 削除状況把握とデプロイトラブル回避に必須 |
cdk diff を使った差分確認 |
破壊的変更の事前検知の基本 |
そりゃ記事も長くなるわ。
ただし将来的に上流を任せられそうであればコード開発を血眼になってやるよりこの手のインフラの技術検証を空き時間にやっておかないと今日日きびしいかもしれないな。
まとめ
とりあえず構築のノウハウは終わり。あとは繰り返してやるだけ。本来は手詰みでこれやってからのIoC
でCDK
にコードを落としていくのがよいが、時間ない場合はいきなりterraform
だのCDK
触らせられる事もあるんでしょうなあ...CDK
でやる場合はネットワーク(VPC)の構築の詳細がほぼ自動になっているので最低限AZとかサブネットとかその辺理解しておかないと運用でハマるかもしれないです。
今回はNATもないですしね。
次回はさすがにもうちょいまともなイメージの作り方とか、CI/CD
をぶちこんだ開発サイクルとか書いて終わりにしたいメリねえ...
Discussion