Pulumiを用いたECS Fargate環境の構築をご紹介

こんにちは。フリージアの佐久間です。
前回は、Pulumiとkubectlを用いたEKS環境構築方法をご紹介しました。
今回は、PulumiでのECSFargate環境構築方法をご紹介しようと思います。

登場人物は以下の通りです。

name description
springboot Webアプリケーション
jib DockerイメージBuilder
ecs awsで提供されているコンテナオーケストレーション環境
pulumi Web技術でコーディング出来るIaCツール
ecr awsのdockerレジストリー

タスクは以下の通りです。

  • springbootアプリケーションを用意
  • jibを使って、Dockerイメージ作成
  • pulumiのセットアップ
  • aws-cliのセットアップ
  • pulumiで、VPCとECRを構築
  • jibを作った、DockerイメージをECRにアップロード
  • pulumiで、ECSを構築&リリース

前回の記事でVPCやECRの構築、Dockerイメージアップロードなどはご紹介したので、
今回は、「pulumiで、ECSを構築&リリース」のみご紹介しようと思います。

その他のタスクに関しては前回の記事をご覧ください。
それでは始めていきます。

pulumiで、ECSを構築&リリース

今回はECS環境を作成しますので、pulumiのindex.tsを以下のようにしてください。
細かい説明は後ほどいたします。

import * as pulumi from "@pulumi/pulumi";
import * as eks from "@pulumi/eks";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import {VpcArgs} from "@pulumi/awsx/ec2";

const config = new pulumi.Config()
// ECRリポジトリー作成
const repository = new aws.ecr.Repository(config.require('ECR_REPOSITORY_NAME'), {
  name: config.require('ECR_REPOSITORY_NAME'),
  imageTagMutability: 'IMMUTABLE'
})
// VPC作成
const vpc = new awsx.ec2.Vpc(config.require('VPC_NAME'), {
  numberOfAvailabilityZones: 2, // ← サブネットの数指定。アベイラビリティゾーン分だけpublic/privateサブネットが作成される
  instanceTenancy: 'default',
  numberOfNatGateways: 1, // ← NatGateway数の指定
  cidrBlock: config.require('VPC_CIDR_BLOCK'), // VPCのCIDR指定
  tags: { Name: config.require('VPC_NAME') } // VPCの名前
} as VpcArgs);

<!-- ECS環境構築はここから -->
// create ecs cluster
const cluster = new awsx.ecs.Cluster(config.require('ECS_CLUSTER_NAME'), {
  vpc: vpc,
  name: config.require('ECS_CLUSTER_NAME')
} as ClusterArgs)
// defined target group
const targetGroup = new awsx.lb.ApplicationTargetGroup(`${config.require('ECS_CLUSTER_NAME')}-tg`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-tg`,
  vpc: vpc,
  protocol: 'HTTP',
  port: 8080,
  healthCheck: {
    path: '/actuator/health',
    timeout: 5,
    interval: 60
  } as ApplicationTargetGroupHealthCheck
})
// defined alb listener
const ContainerListener = targetGroup.createListener(`${config.require('ECS_CLUSTER_NAME')}-listener-8080`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-listener-8080`,
  vpc: vpc,
  protocol: 'HTTP',
  port: 8080
} as ApplicationListenerArgs)
const listener = targetGroup.createListener(`${config.require('ECS_CLUSTER_NAME')}-listener-443`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-listener-443`,
  vpc: vpc,
  protocol: 'HTTPS',
  port: 443,
  external: true,
  certificateArn: config.requireSecret('CERTIFICATE_ARN')
} as ApplicationListenerArgs)
// create service
const apiService = new awsx.ecs.FargateService(`${config.require('ECS_CLUSTER_NAME')}-service`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-service`,
  cluster: cluster,
  subnets: vpc.privateSubnets,
  securityGroups: cluster.securityGroups,
  desiredCount: 1,
  taskDefinitionArgs: {
    containers: {
      apiServer : {
        image: repository.repositoryUrl.apply((repositoryUrl) => `${repositoryUrl}:${config.require('ECR_IMAGE_VERSION')}`),
        cpu: 512,
        memory: 1024,
        portMappings: [ ContainerListener ],
        environment: [
          {name: 'SPRING_PROFILES_ACTIVE', value: 'development'}
        ]
      }
    }
  }
})
// define route53
const zone = aws.route53.getZone({ name: config.require('DOMAIN_NAME') })
const route = new aws.route53.Record(config.require('DOMAIN_NAME'), {
  name: `stg.api.${config.require('DOMAIN_NAME')}`,
  zoneId: zone.id,
  type: 'A',
  aliases: [
    {
      name: listener.loadBalancer.loadBalancer.dnsName.apply((v) => `dualstack.${v}`),
      zoneId: listener.loadBalancer.loadBalancer.zoneId,
      evaluateTargetHealth: true,
    },
  ],
} as RecordArgs)

今回は、PulumiのみでECS Fargateを外部公開するところまでやっています。
上記のスクリプトでのデプロイは、ローリングアップデートとなっています。
AWSCodeDeployを利用すれば、ブルーグリーンデプロイも実現出来ます。

詳細解説

クラスター作成

// create ecs cluster
const cluster = new awsx.ecs.Cluster(config.require('ECS_CLUSTER_NAME'), {
  vpc: vpc,
  name: config.require('ECS_CLUSTER_NAME')
} as ClusterArgs)

ここでは既存のVPCにECSクラスターを作成しています。
立ててるだけで月額$144かかるEKSクラスターと違って、ECSクラスターはクラスター単位の料金は発生しないようです。

ロードバランサー設定

// defined target group
const targetGroup = new awsx.lb.ApplicationTargetGroup(`${config.require('ECS_CLUSTER_NAME')}-tg`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-tg`,
  vpc: vpc,
  protocol: 'HTTP',
  port: 8080,
  healthCheck: {
    path: '/actuator/health',
    timeout: 5,
    interval: 60
  } as ApplicationTargetGroupHealthCheck
})
// defined alb listener
const ContainerListener = targetGroup.createListener(`${config.require('ECS_CLUSTER_NAME')}-listener-8080`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-listener-8080`,
  vpc: vpc,
  protocol: 'HTTP',
  port: 8080
} as ApplicationListenerArgs)
const listener = targetGroup.createListener(`${config.require('ECS_CLUSTER_NAME')}-listener-443`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-listener-443`,
  vpc: vpc,
  protocol: 'HTTPS',
  port: 443,
  external: true,
  certificateArn: config.requireSecret('CERTIFICATE_ARN') // secret configを読み込む
} as ApplicationListenerArgs)

ここでは、ApplicationLoadBlancerとApplicationListener、ApplicationTargetGroupを作成しています。
それぞれの関係性は以下のようになっています。

www(インターネット)
↓
ApplicationLoadBlancer
↓
ApplicationListener
↓
ApplicationTargetGroup
↓
FargateService

外部にはhttpsで公開したいのですが、内部のヘルスチェックはhttpで行われるので、
ApplicationListenerをそれぞれ用に2つ作成しています。
ヘルスチェックエンドポイントはデフォルト「/health」になっていますが、
変更したい場合はApplicationTargetGroupのhealthCheck.pathを修正してください。

ECSクラスターにコンテナデプロイ

const apiService = new awsx.ecs.FargateService(`${config.require('ECS_CLUSTER_NAME')}-service`, {
  name: `${config.require('ECS_CLUSTER_NAME')}-service`,
  cluster: cluster,
  subnets: vpc.privateSubnets,
  securityGroups: cluster.securityGroups,
  desiredCount: 1,
  taskDefinitionArgs: {
    containers: {
      apiServer : {
        image: repository.repositoryUrl.apply((repositoryUrl) => `${repositoryUrl}:${config.require('ECR_IMAGE_VERSION')}`),
        cpu: 512,
        memory: 1024,
        portMappings: [ ContainerListener ],
        environment: [
          {name: 'SPRING_PROFILES_ACTIVE', value: 'development'}
        ]
      }
    }
  }
})

ここでは、ECSクラスターにFargateService定義とタスク定義を作成しています。
cpuとmemoryに与える数値でインスタンス料金が変動します。ご自身のプロダクトごとに適宜、設定してください。
また、FargateServiceはCPUとMemoryに設定出来る値が決まっています。
下記を参考にしてください。
CPUとMemoryに設定出来る値一覧

立ち上げるコンテナの環境変数を指定したい場合は「environment」に定義することも可能です。

サービスを外部公開

const zone = aws.route53.getZone({ name: config.require('DOMAIN_NAME') })
const route = new aws.route53.Record(config.require('DOMAIN_NAME'), {
  name: `stg.api.${config.require('DOMAIN_NAME')}`,
  zoneId: zone.id,
  type: 'A',
  aliases: [
    {
      name: listener.loadBalancer.loadBalancer.dnsName.apply((v) => `dualstack.${v}`),
      zoneId: listener.loadBalancer.loadBalancer.zoneId,
      evaluateTargetHealth: true,
    },
  ],
} as RecordArgs)

最後に作成されたサービスをRoute53経由で公開して完了となっています。

まとめ

Pulumiを使って、EKSもECSも構築してみましたが、弊社ではECSを利用することになりそうです。
理由は2台しかサーバーを運用しない状態だと、ECSの方が圧倒的に安かったからです。
(個人的にはそのうちKubenates載せ替えたいです。だって何かカッコいいんだもの。)

皆さんも、是非Pulumiを利用して、ECS環境やEKS環境を構築してクラウドネイティブな世界を体感してみてはいかがでしょうか?

最後に

本記事を見て、ご意見やご指摘等ございましたら、「[email protected]」まで是非ご連絡ください。

Twitterもやってます。ご興味ありましたら、フォローしてください。
Twitter:asakuma0401