正月休みということで、時間を使ってやるようなものを消化していこうと思っています。
今回は AWS CDK(Cloud Development Kit) で Fargate で nginx を建ててみます。
以下で行ったことは、Githubに上げてあります。
バージョン
$ aws --version aws-cli/2.4.7 Python/3.9.9 Darwin/19.6.0 source/x86_64 prompt/off $ node --version v16.10.0 $ cdk --version 2.3.0 (build beaa5b2)
なお、CDKで利用する言語はTypeScriptを選択しています。
CDK をインストールする
$ npm i -g aws-cdk
cdk init
が空のディレクトリでしか実行できないため、グローバルにインストールします。
CDK プロジェクトを作成する
cdk init
でプロジェクトを作成します。プロジェクトを作成するディレクトリで実行します。
$ mkdir cdk_fargate_example && cd cdk_fargate_example $ cdk init app --language typescript Applying project template app for typescript # Welcome to your CDK TypeScript project! This is a blank project for TypeScript development with CDK. The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `cdk deploy` deploy this stack to your default AWS account/region * `cdk diff` compare deployed stack with current state * `cdk synth` emits the synthesized CloudFormation template Initializing a new git repository... Executing npm install... ✅ All done!
実行すると以下のようなプロジェクトが作成されます。
. ├── README.md ├── bin │ └── cdk_fargate_example.ts ├── cdk.json ├── jest.config.js ├── lib │ └── cdk_fargate_example-stack.ts ├── node_modules ├── package-lock.json ├── package.json ├── test └── tsconfig.json
CDK v1では、各リソースごとにモジュールをインストールする必要がありましたが、v2では不要です。
環境変数を .env ファイルで指定できるようにする、dotenv だけ今回は入れておきます(なくても特に問題ありません)。
$ npm i dotenv
最後に、自動でビルドされるように npm run watch
を実行しておきます。
$ npm run watch > cdk_fargate_example@0.1.0 watch > tsc -w [15:03:43] Starting compilation in watch mode... [15:03:51] Found 0 errors. Watching for file changes.
リージョンを設定
#!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { CdkFargateExampleStack } from '../lib/cdk_fargate_example-stack'; import * as dotenv from 'dotenv'; dotenv.config(); const app = new cdk.App(); new CdkFargateExampleStack(app, 'CdkFargateExampleStack', { // デフォルトのリージョンを ap-northeast-1 にする // 環境変数の指定があれば、そちらを優先する env: { region: process.env.CDK_DEFAULT_REGION ?? 'ap-northeast-1' } // <= ここを追加 });
CDK Bootstrap を実行する
CloudFormationで利⽤するデプロイ⽤のS3 バケットを作成します。
リージョンで初めて実行する際には必要になります。
$ cdk bootstrap ⏳ Bootstrapping environment aws://xxxxxxxxxxxx/ap-northeast-1... Trusted accounts for deployment: (none) Trusted accounts for lookup: (none) Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize. ✅ Environment aws://xxxxxxxxxxxx/ap-northeast-1 bootstrapped (no changes).
リソースの設定を書いていく
ここからリソースの設定を書いていくことになりますが、今回はサンプルということで lib/cdk_fargate_example.ts
にすべてを書いていきます。
実際の本番環境などではより多くのリソースを管理したりすることになるため、複数のファイルでクラスに分割して、必要な変数を bin/cdk_fargate_example.ts
でコンストラクタに渡すような形にもできます。
以降は基本的に lib/cdk_fargate_example.ts
の CdkFargateExampleStack
クラスのコンストラクタに記載していきます。
import { Construct } from 'constructs'; import { Stack, StackProps, RemovalPolicy, Duration, aws_ec2 as ec2, aws_ecs as ecs, aws_ecr as ecr, aws_elasticloadbalancingv2 as elb, aws_logs as logs, aws_servicediscovery as servicediscovery, aws_iam as iam } from 'aws-cdk-lib'; import { SubnetType } from 'aws-cdk-lib/aws-ec2'; export class CdkFargateExampleStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const prefix = 'cdk-fargate-example' // ここに記載していく } }
デプロイの実行
書いていく前にデプロイの実行の仕方を記載しておきます。
cdk synth
まず、 cdk synth
です。
cdk synth
を実行するとCloud Formationのテンプレートが出力されます。
Cloud Formation に詳しければ、ここでどのような設定でリソースが作成されるかを確認することができます。
$ cdk synth Resources: appContainerRepoCBCDBB42: Type: AWS::ECR::Repository Properties: ... 省略... Rules: CheckBootstrapVersion: Assertions: - Assert: Fn::Not: - Fn::Contains: - - "1" - "2" - "3" - "4" - "5" - Ref: BootstrapVersion
cdk diff
次に cdk diff
です。
現在の環境から実際に作成される差分を確認できます。
$ cdk diff (以下の出力は一部省略しています) Stack CdkFargateExampleStack IAM Statement Changes ┌───┬──────────────────────────────────────┬────────┬──────────────────────────────────────┬────────────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼──────────────────────────────────────┼────────┼──────────────────────────────────────┼────────────────────────────────────────┼───────────┤ │ + │ ${logGroup.Arn} │ Allow │ logs:CreateLogStream │ AWS:${taskExecutionRole} │ │ │ │ │ │ logs:PutLogEvents │ │ │ ├───┼──────────────────────────────────────┼────────┼──────────────────────────────────────┼────────────────────────────────────────┼───────────┤ └───┴──────────────────────────────────────┴────────┴──────────────────────────────────────┴────────────────────────────────────────┴───────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Parameters [+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/xxxxxxxxx/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"} Resources [+] AWS::ECR::Repository appContainerRepo appContainerRepoXXXXXXXX
cdk deploy
cdk deploy
で実際に適用します。
最後に確認がでるので、 y
で確定すると、適用が開始されます。
$ cdk deploy ...変更内容の表示 ... Do you wish to deploy these changes (y/n)?
VPCを作成する
const vpc = new ec2.Vpc(this, 'Vpc', { cidr: '10.0.0.0/16', enableDnsHostnames: true, enableDnsSupport: true, natGateways: 0, subnetConfiguration: [ { cidrMask: 28, name: 'public', subnetType: SubnetType.PUBLIC, }, ] });
今回はNATゲートウェイを作成しないですむように、パブリックサブネットのみでNATゲートウェイを0にします。
ECRのリポジトリを追加する
const appContainerRepo = new ecr.Repository(this, 'appContainerRepo', { repositoryName: `${prefix}-app`, imageScanOnPush: true, imageTagMutability: ecr.TagMutability.MUTABLE });
ALBを追加する
const albSecurityGroup = new ec2.SecurityGroup(this, 'albSecurityGroup', { securityGroupName: `${prefix}-alb-security-group`, vpc: vpc, }); albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80)); const alb = new elb.ApplicationLoadBalancer(this, 'alb', { vpc: vpc, internetFacing: true, securityGroup: albSecurityGroup, vpcSubnets: { subnets: vpc.publicSubnets }, }); const listener = alb.addListener('albListener', { port: 80 });
ALBのセキュリティグループ、ALB、リスナーを作成します。
Fargateを追加する
// クラスタ const cluster = new ecs.Cluster(this, 'cluster', { vpc: vpc, }); // サービスのセキュリティグループ const serviceSecurityGroup = new ec2.SecurityGroup( this, 'serviceSecurityGroup', { securityGroupName: `${prefix}-service-security-group`, vpc: vpc, } ); serviceSecurityGroup.addIngressRule( albSecurityGroup, ec2.Port.allTcp() ); // サービスディスカバリ const cloudmapNamespace = new servicediscovery.PrivateDnsNamespace( this, 'namespace', { name: 'cdk.ecs.local', vpc: vpc, } ); // ECSExecのポリシー const ecsExecPolicyStatement = new iam.PolicyStatement({ sid: 'allowECSExec', resources: ['*'], actions: [ 'ssmmessages:CreateControlChannel', 'ssmmessages:CreateDataChannel', 'ssmmessages:OpenControlChannel', 'ssmmessages:OpenDataChannel', 'logs:CreateLogStream', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams', 'logs:PutLogEvents', ], }); // タスクのロール const taskRole = new iam.Role(this, 'taskRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); taskRole.addToPolicy(ecsExecPolicyStatement); // タスク実行のロール const taskExecutionRole = new iam.Role(this, 'taskExecutionRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), managedPolicies: [ { managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', }, ], }); // ロググループ const logGroup = new logs.LogGroup(this, 'logGroup', { logGroupName: `${prefix}`, removalPolicy: RemovalPolicy.DESTROY, }); // タスク定義 const taskDefinition = new ecs.FargateTaskDefinition( this, 'taskDefinition', { memoryLimitMiB: 512, cpu: 256, executionRole: taskExecutionRole, taskRole: taskRole, } ); // イメージ(リポジトリから指定) const image = ecs.ContainerImage.fromEcrRepository( ecr.Repository.fromRepositoryName(this, 'appImage', `${prefix}-app`) ); // コンテナをタスク定義に追加 taskDefinition.addContainer('container', { image: image, containerName: 'app', logging: ecs.LogDriver.awsLogs({ streamPrefix: prefix, logGroup: logGroup, }), portMappings: [ { containerPort: 80, hostPort: 80, protocol: ecs.Protocol.TCP, }, ], }); // サービス const fargateService = new ecs.FargateService( this, 'fargateService', { cluster: cluster, desiredCount: 1, assignPublicIp: true, taskDefinition: taskDefinition, enableExecuteCommand: true, cloudMapOptions: { cloudMapNamespace: cloudmapNamespace, containerPort: 80, dnsRecordType: servicediscovery.DnsRecordType.A, dnsTtl: Duration.seconds(10), }, securityGroups: [serviceSecurityGroup], } ); // サービスでALBのターゲットに登録する fargateService.registerLoadBalancerTargets( { containerName: 'app', containerPort: 80, newTargetGroupId: 'Ecs', listener: ecs.ListenerConfig.applicationListener(listener, { protocol: elb.ApplicationProtocol.HTTP }), }, );
ここまで書いた時点で、 cdk deploy
してもよいのですが、ECRにイメージを追加していないため、Fargateのサービス作成で失敗するかと思います。そのため、まずは desiredCount
を 0
にしておきましょう。もしくは、一旦ECRのみを作成してイメージをPushしてから、のこりを適用することでも対応できます。
イメージをECRにPushする
ECRへのイメージのPushは今回の範囲外なのと、そこまで難しくないので、以下のドキュメントを参考にしてください。
実際に確認してみる
ここまでで、準備は完了しているので、もしdesiredCount
を 0
にしている場合は、 1
にして、 cdk deploy
をしてみます。
デプロイが完了して、ALBにアクセスすると、無事にnginxにアクセスできることが確認できます。
作成したリソースを削除する
最後に今回は検証だったので、作成したリソースを削除します。
cdk destroy
をすると、リソースを一括で削除できます。ただし、今回設定したうち、以下については削除されないようなので注意してください。
- ECRは削除されない
- CloudFormationのデプロイ用S3は削除されない
また、今回直接作成はしていませんが、S3もデフォルトでは削除されないようです。
(CDK v1時点の記事なので、変わっているかもしれません)
まとめ
今回は、AWS CDKでFargateを建ててみました。
自力ですべてを書こうとするとなかなか大変ですが、ドキュメントとWebを参考にしながらであれば、とりあえずの構築は1日程度でできそうです。
Web上の情報などは、CDK v1 の情報も多く、ちゃんと公式のドキュメントを参考にするほうが良さそうです。
参考
CDKを学びはじめる場合、まずは以下のWorkshopをおすすめします(私もこちらからスタートしましたが、だいたい3〜4時間ほどで終わるかと思います)。
cdkworkshop.com