TerraformでAWSサーバーレスなサービスのインフラ構築をコード化する

最近、TerraformでAWSサーバーレスなサービスのインフラ構築をコード化する機会があったので理解を深めるためにプライベートでも作ってみました。

AWSサーバーレスなサービスの全体像

今回、Terraformで構築をコード化する対象の環境です。SPA(Single Page Application)な構成としました。

f:id:nihma:20180814150726p:plain

大きくWebの部分とAPIの部分、その他の部分に分かれています。

Webの部分:

  • ユーザからアクセスを受ける CloudFront
  • SPAのHTMLを配置する S3 bucket

APIの部分:

  • SPAのHTMLからアクセスを受ける CloudFront
  • APIの機能を提供する Lambda Function
  • CloudFront と Lambda Function を橋渡しする API Gateway

その他の部分:

  • 認証のための Cognito
  • セキュリティ対策のための WAF
  • データ永続化のための Dynamo DB

インフラ構築の流れ

セットアップ用のCodeBuild

まず起点となるセットアップ用のCodeBuildです。

f:id:nihma:20180814162506p:plain

最初にTerraformを使って下記を構築します。

  • サービスのStaging環境を構築するCodeBuild
  • サービスのProduction環境を構築するCodeBuild
  • 上記のCodeBuildを順次実行するCodePipeline

そして、最後に構築したCodePipelineを実行してサービスのStaging環境とProduction環境を構築します。

Staging環境の構築後には承認アクションが必要なようにしました。Staging環境の確認で問題が見つかった場合は承認を却下して以降の構築を停止することもできます。

サービスの環境を構築するCodeBuild

次はサービスのStaging環境を構築するCodeBuildおよびProduction環境を構築するCodeBuildです。こちらもTerraformを使います。

f:id:nihma:20180814162517p:plain

まず、他のリソースに依存しない、WebやAPIでは無い部分の下記を構築します。

次にWebの下記を構築します。ここで得られたCloudFront(Web)のURLは後続で構築するAPIのCORSに設定します。

  • S3 bucket(Web)
  • CloudFront(Web)

そして、API部分の構築・デプロイを行うCodeBuildを構築・実行します。これで下記が構築・デプロイされます。下記の構築・デプロイには Serverless Framework を使います。ここで得られたAPI Gatewayドメイン名は後続で構築するCloudFront(API)の紐付け先に設定します。

次にAPIのCloudFrontを構築します。ここで得られたCloudFront(API)のURLは後続で構築するWebのHTMLにAPIの呼び出し先URLとして設定します。

  • CloudFront(API)

最後にWeb部分のデプロイを行うCodeBuildを構築および実行します。これで下記がデプロイされます。ここは Angular を使います。

  • HTML(Web)

Production環境の場合はこのタイミングで下記を構築します。これらもサービスのCodePipelineと同様にStaging環境の構築後には承認アクションがあります。

  • APIのStaging環境を構築・デプロイするCodeBuildとProduction環境を構築・デプロイするCodeBuildを順次実行するCodePipeline
  • WebのStaging環境にデプロイするCodeBuildとProduction環境にデプロイするCodeBuildを順次実行するCodePipeline

Staging環境の構築とProduction環境の構築を結ぶCodePipeline

サービス全体およびAPIの部分、Webの部分はそれぞれにCodePipelineが構築されます。

f:id:nihma:20180814173736p:plain

それぞれCodeCommitのmasterブランチに変化があると実行されます。

APIの部分とWebの部分のCodePipelineがあることでAPIの部分だけ、Webの部分だけでのデプロイが行えます。サービス全体のCodePipelineはAPIの部分とWebの部分も構築・デプロイを行います。

実装内容

コードをGitHubにアップしました。

aws-sls-spa-sample-apiaws-sls-spa-sample-webaws-sls-spa-sample-terraform でインフラ構築するための必要な最小限の実装しかありません。

使っているツールのバージョンは下記です。

  • Terraform: 0.11.7
  • Serverless Framework: 1.30.0
  • Angular: 6.0.8

インフラ部分:aws-sls-spa-sample-terraform

大まかなディレクトリ構成です。

.
├── bin ... シェルなど
├── buildspec_setup.yml      ... セットアップ用のCodeBuild向け
├── buildspec_staging.yml    ... サービスのStaging構築CodeBuild向け
├── buildspec_production.yml ... サービスのProduction構築CodeBuildCodeBuild向け
├── environments ... 構築フローに沿ったTerraformの定義
│   │── setup   ... セットアップ用
│   └── service ... サービス用
│       ├── base          ... その他の部分
│       │   ├── pre
│       │   └── after_api
│       ├── api           ... API部分
│       │   ├── staging
│       │   └── production
│       │── web           ... Web部分
│       │   ├── staging
│       │   └── production
│       └── pipeline      ... API部分/Web部分のCodePipeline
└── modules      ... 各リソースのTerraformの定義
    ├── cloudfront
    ├── cloudwatch_event
    ├── codebuild
    ├── codepipeline
    ├── cognito
    ├── dynamodb
    ├── iam
    ├── s3
    └── waf

CodeBuildが buildspec_xxxx.yml を呼び出して構築をしていきます。

それぞれのTerraformの定義は リファレンス を参照しつつ基本的に consoleで作成したリソースのterraform import を実施した結果から作りました。

例えばaws_cognito_user_poolならこんな感じです。

$ cat terraform.tfvars
access_key = "XXXXXXXXX"
secret_key = "XXXXXXXXX"
region     = "ap-northeast-1"

# 空の定義を用意する
$ cat main.tf
variable "access_key" {}
variable "secret_key" {}
variable "region"     {}

provider "aws" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region     = "${var.region}"
}

resource "aws_cognito_user_pool" "hoge" {
}

$ terraform init
# 空で定義したリソースをimportする
$ terraform import aws_cognito_user_pool.hoge ap-northeast-1_XXXXXX
# tfstateに定義がimportされるので参照して定義を作っていく...
$ cat terraform.tfstate

API部分:aws-sls-spa-sample-api

ほぼ Serverless Framework の aws-nodejs-typescript をテンプレートにして作成した素のままの状態です。

$ serverless create --template aws-nodejs-typescript

これに aws-sls-spa-sample-terraform で必要な buildspec.yml の追加と serverless.yml の修正を行いました。

Web部分:aws-sls-spa-sample-web

こちらもほぼ Angular で ng new した素のままの状態です。

$ ng new aws-sls-spa-sample-web --style=scss --routing

これに aws-sls-spa-sample-terraform で必要な buildspec.yml の追加と angular.json の修正を行いました。

使い方

セットアップ用のCodeBuildを手動で構築・実行してサービスのインフラ構築を行うまでの手順です。リソース名は例のため実際に実施するときは変える必要があるので注意が必要です。

1. GitHubリポジトリをCodeCommitに持ってくる

CodeCommitにリポジトリを作成します。

# spa infra repository...
$ aws codecommit create-repository --repository-name foobar-sample-spa-infra
# spa api repository...
$ aws codecommit create-repository --repository-name foobar-sample-spa-api
# spa web repository...
$ aws codecommit create-repository --repository-name foobar-sample-spa-web

作成したCodeCommitのリポジトリGitHubリポジトリをpushする。

# spa infra repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-terraform.git sample-spa-infra
$ cd sample-spa-infra
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra
$ cd ..
# spa api repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-api
$ cd ..
# spa web repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-web.git sample-spa-web
$ cd sample-spa-web
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-web
$ cd ..

2. セットアップ用のCodeBuildを動かすために必要なリソースを作る

CodePipelineの承認アクションで使うSNSのトピックを作成する。

$ aws sns create-topic --name foobar-sample-spa-approval-topic
$ aws sns subscribe --topic-arn arn:aws:sns:ap-northeast-1:<account-id>:foobar-sample-spa-approval-topic \
                    --protocol email \
                    --notification-endpoint <your email>
# and confirm...
$ aws sns confirm-subscription --topic-arn arn:aws:sns:ap-northeast-1:<account-id>:foobar-sample-spa-approval-topic \
                               --token <token value>

Terraformの状態管理ファイルを保存するS3のバケットを作成する。S3のバケット名は全世界でユニークにする必要があるので注意が必要です。

$ aws s3 mb s3://foobar-sample-spa-terraform-state --region ap-northeast-1

CodeBuildを動かすために必要なIAMロールを作成する。 TF_VAR_service_name に指定した文字列が作成されるリソースのプレフィックスになるので他と被らない、長すぎない文字列を使うように注意が必要です。

$ cat Trust-Policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
$ aws iam create-role --role-name foobar-sample-spa-setup-codebuild \
                      --assume-role-policy-document file://Trust-Policy.json
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
                             --role-name foobar-sample-spa-setup-codebuild

セットアップ用のCodeBuildを作成する。

$ cat Source.json
{
  "type": "CODECOMMIT",
  "location": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra",
  "buildspec": "buildspec_setup.yml"
}
$ cat Artifacts.json
{
  "type": "NO_ARTIFACTS"
}
$ cat Environment.json
{
  "type": "LINUX_CONTAINER",
  "image": "aws/codebuild/ubuntu-base:14.04",
  "computeType": "BUILD_GENERAL1_SMALL",
  "environmentVariables": [
    {
      "name": "TF_VAR_service_name",
      "value": "foobar-sample-spa",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_approval_sns_topic_arn",
      "value": "arn:aws:sns:ap-northeast-1:<account-id>:foobar-sample-spa-approval-topic",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_s3_bucket_terraform_state_id",
      "value": "foobar-sample-spa-terraform-state",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_codecommit_infra_repository",
      "value": "foobar-sample-spa-infra",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_codecommit_api_repository",
      "value": "foobar-sample-spa-api",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_codecommit_web_repository",
      "value": "foobar-sample-spa-web",
      "type": "PLAINTEXT"
    }
  ]
}
$ aws codebuild create-project --name foobar-sample-spa-setup \
                               --source file://Source.json \
                               --artifacts file://Artifacts.json \
                               --environment file://Environment.json \
                               --service-role arn:aws:iam::<account-id>:role/foobar-sample-spa-setup-codebuild

3. セットアップ用のCodeBuildを実行する

セットアップ用のCodeBuildを実行すると環境が順次、作成されていきます。リソースはそれぞれお金がかかるので注意が必要です。

$ aws codebuild start-build --project-name foobar-sample-spa-setup

WebのエンドポイントはCloudFrontのconsoleから探せます。

f:id:nihma:20180818173759p:plain

httpsでのみアクセスできます。反映まで少しかかるのでアクセスできるようになるまで少し待つ必要があります。

https://xxxxxx.cloudfront.net

中身がないのでAngularのデフォルトページです。

f:id:nihma:20180816220608p:plain

もしここでアクセスした時に307リダイレクトが起きたらだいぶ待つ必要があるみたいです...(何度か遭遇しました https://stackoverflow.com/questions/38706424/aws-cloudfront-returns-http-307-when-origin-is-s3-bucket

まとめ

今回はTerraformでAWSサーバーレスなサービスのインフラ構築をするサンプルを作ってみました。心残りは下記です。

  • インフラ構成をもうちょい見直したい
  • IAM権限が適当なのでちゃんと絞りたい
  • API/Webを動くようにしたい
  • Terraformは保守大変・インフラ構成変更時の挙動が怪しいなどあるので他の選択肢も試したい
  • AWS以外も試したい