🐥

Hello AWS (part 4:AWS CDKを用いたIaC)

に公開

動機

Part 1から3を通じて、主にAWSコンソールを利用しながら手作業でシステムを構築してきた。
実際に自分で手を動かして(エラーと向き合いながら)組み上げていくプロセスは初学者として非常に大切だと感じる一方で、マニュアルゆえの設定忘れや間違いも数多くあり、特に実務で扱う上では再現性の観点からも自動化したいと感じた。

目的

  • 手作業に起因する間違いや煩雑さを解消するため、AWS CDKを用いてシステム構築のプロセスをスクリプト(TypeScript)で定義する。このアプローチはInfrastructure as Code(IaC)と呼ばれる。
  • Part 1で扱ったミニマルな構成をスクリプトを用いて再現する。

構成

Part 1と全く同じなので割愛する。

手順

ローカルを汚さないため、Docker上で環境を構築することとする。

ログイン

あらかじめホストでログインし、環境変数をコンテナに対して設定することで、コンテナ内からAWSに構成を反映させられるようにする。
ホストでのCLIログインのやり方はPart 1で触れたように様々だが、結果として

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_SESSION_TOKEN

が設定されている形であればよい。
その後

docker run \
  --rm \
  -it \
  -v ${PWD}:/hello_aws \
  -w /hello_aws \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_SESSION_TOKEN \
  -e AWS_DEFAULT_REGION=ap-northeast-1 \
  node:latest bash

などでコンテナ(NodeJSコンテナ)に環境変数を持ち込みながら入る。
なおここでホストのworking directoryをマウントしている。

以下のコマンドは全てDockerコンテナ内で実行する。

AWS CDKの導入

新しいディレクトリ(以下注意参照)を作成し、内部で以下のコマンド(AWS CDKの初期化コマンド)を実行する。

npx cdk init app --language typescript

Git関連ファイルも生成されるので、この時点でコミットしておくとよい。

スタックの定義

デプロイでAWSに反映される単位をスタックと呼ぶ(スタックは、CloudFormationにおける「リソースの集合」のようなもの)。
アプリケーションをいくつかのスタックに分割してデプロイすることもできるが、今回は小規模であることから一回で全てをデプロイすることにする。

以下のコードを用いる。
やっていることはPart 1で触れた内容を全てコードとして表現している(リソースを定義し、それぞれの関係を伝える)だけである。
なおECRへのDockerイメージ(hello-aws/web)の登録は事前に済ませてあるものとする。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs from 'aws-cdk-lib/aws-ecs';

export class HelloAwsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const vpc = new ec2.Vpc(this, 'HelloAwsVpc', {
      maxAzs: 1,
      natGateways: 0,
      subnetConfiguration: [
        {
          name: 'public-subnet',
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });
    const sg = new ec2.SecurityGroup(this, 'HelloAwsSecurityGroup', {
      vpc,
      description: 'Allow HTTP in and all outbound',
      allowAllOutbound: true,
    });
    sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');
    const cluster = new ecs.Cluster(this, 'HelloAwsCluster', {
      vpc,
    });
    const repository = ecr.Repository.fromRepositoryName(
      this,
      'HelloAwsEcrRepo',
      'hello-aws/web',
    );
    const taskDef = new ecs.FargateTaskDefinition(this, 'HelloAwsTaskDef', {
      memoryLimitMiB: 512,
      cpu: 256,
    });
    taskDef.addContainer('HelloAwsContainer', {
      image: ecs.ContainerImage.fromEcrRepository(repository),
      portMappings: [{ containerPort: 80 }],
      logging: ecs.LogDriver.awsLogs({ streamPrefix: 'hello-aws' }),
    });
    new ecs.FargateService(this, 'HelloAwsService', {
      cluster,
      taskDefinition: taskDef,
      assignPublicIp: true,
      securityGroups: [sg],
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
    });
  }
}

必要な定義は全てここに書かれている。

型チェック

tscを用いた型チェックは

npm run build

で行える。
なおデフォルトだとトランスパイル後のJavaScriptソースが吐き出される。基本的に必要ないので、tsconfig.jsoncompilerOptions"noEmit": trueを加えてしまってもよいかもしれない。
もしくは

npx tsc --noEmit

デプロイ

以下のコマンドで内容をAWS上に反映する:

npx cdk deploy

削除

Deployコマンドで構築した環境は依存関係含めて全て次のコマンドで削除が可能である:

npx cdk destroy

それなりに時間がかかる場合があるので辛抱して待つ。
なおデプロイした後でAWS CDKを経ずに(コンソールなどで)編集したりした場合はうまくいかない場合もある。

Discussion