Terraform v0.8.6とCodeBuildを利用したServerless Frameworkのデプロイ

Terraform

はじめに

こんにちは、中山です。

次期Terraformのリリースバージョンであるv0.8.6でAWS CodeBuild用リソースがマージされました。今回導入されたリソースは以下の通りです。

リソース名 用途
aws_codebuild_project CodeBuildプロジェクトの作成

早速使ってみたので本エントリでまとめたいと思います。サンプルとなるコードをGitHubに作成しました。ご自由にお使いください。詳細な内容は後述しますが、Serverless Frameworkで作成したサンプルアプリをCodeBuildでビルド/デプロイするといった内容です。

執筆時点(2017/02/04)ではまだv0.8.6はリリースされていないため、自分でTerraformのバイナリをコンパイルしておいてください。このPRが取り込まれたコミットハッシュ値は「04102574535d9e3d94d60fbac8686fe97f4e4ca8」です。

CodeBuild関連リソースの引数

新規に導入されたCodeBuild関連リソースで設定可能な各種引数は以下の通りです。現状ドキュメントが用意されていないようなので、ソースコードにもとづいています。基本的にCloudFormationのAWS::CodeBuild::Projectリソースとほぼ同じです。このリソースについては以下のエントリでまとめているので参考にしていただければと思います。

設定 意味 必須の有無 備考
artifacts プロジェクトで生成したアーティファクトの設定 Yes 詳細は下記参照
description プロジェクトの説明 No
encryption_key アーティファクトを暗号化するためのKMSのARNまたはエイリアス No
environment ビルド環境の設定 Yes 詳細は下記参照
name プロジェクト名 Yes
service_role プロジェクトに関連付けるIAM Role No
source プロジェクトでビルドするソースコードの設定 Yes 詳細は下記参照
timeout ビルドのタイムアウト時間(分) No
tags プロジェクトに関連付けるタグ No

artifacts で指定可能な引数は以下の通りです。

設定 意味 必須の有無 備考
name アーティファクトを設置するディレクトリ No
location アーティファクトを設置する場所 No
namespace_type アーティファクトの設置場所(パス)に加える情報 No
packaging アーティファクトのパッケージ方式 No Zipで固めるかどうか(デフォルトは何もしない)
path name で指定した場所に加えるパス No
type アーティファクトのタイプ Yes

environment で指定可能な引数は以下の通りです。

設定 意味 必須の有無 備考
compute_type ビルド環境のスペック Yes
environment_variable ビルド環境で利用する環境変数 No Key/Value形式で指定
image ビルド環境のコンテナイメージ Yes
type ビルド環境のタイプ Yes 現状 LINUX_CONTAINER のみ指定可能

source で指定可能な引数は以下の通りです。

設定 意味 必須の有無 備考
auth CodeBuildがソースコードにアクセスする際の認証情報 No type (現状 OAUTH のみ)と resource で指定
buildspec buildspec.yml の場所 No
location リポジトリの場所 No
type リポジトリのタイプ Yes

使ってみる

早速使ってみましょう。最終的に以下のような構成になります。

tf-codebuild-1

コード

以下で主要なコードを解説していきます。

  • tf-codebuild-demo/codebuild.tf
resource "aws_codebuild_project" "codebuild" {
  name         = "${var.env}-project"
  service_role = "${aws_iam_role.codebuild.arn}"
  timeout      = 60

  artifacts {
    type     = "S3"
    location = "${random_id.s3.hex}"
  }

  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/nodejs:7.0.0"
    type         = "LINUX_CONTAINER"

    environment_variable = {
      "name"  = "S3"
      "value" = "${random_id.s3.hex}"
    }

    environment_variable = {
      "name"  = "STAGE"
      "value" = "${var.codebuild_config["stage"]}"
    }
  }

  source {
    type     = "GITHUB"
    location = "${var.codebuild_config["source_location"]}"
  }

  tags {
    "Environment" = "${var.env}-project"
  }
}

Serverless FrameworkはNode.js製なので、コンテナイメージには aws/codebuild/nodejs:7.0.0 を利用しています。ビルド環境に指定可能なコンテナイメージの一覧は以下のコマンドで確認できます。

$ aws codebuild list-curated-environment-images \
  --region us-east-1
{
    "platforms": [
        {
            "languages": [
                {
                    "images": [
                        {
                            "name": "aws/codebuild/eb-java-7-amazonlinux-64:2.1.3",
                            "description": "AWS ElasticBeanstalk - Java 7 Running on Amazon Linux 64bit v2.1.3"
                        },
<snip>
  • tf-codebuild-demo/iam.tf
data "aws_iam_policy_document" "codebuild_sts" {
  statement {
    sid     = "CodeBuildAssumeRolePolicy"
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals = {
      type        = "Service"
      identifiers = ["codebuild.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "codebuild" {
  name               = "${var.env}-codebuild-role"
  assume_role_policy = "${data.aws_iam_policy_document.codebuild_sts.json}"
}

data "aws_iam_policy_document" "codebuild_policy" {
  statement {
    sid       = "CodeBuildCFnPolicy"
    effect    = "Allow"
    resources = ["*"]
    actions   = ["cloudformation:*"]
  }
}

resource "aws_iam_role_policy" "codebuild" {
  name   = "${var.env}-codebuild-policy"
  role   = "${aws_iam_role.codebuild.id}"
  policy = "${data.aws_iam_policy_document.codebuild_policy.json}"
}

resource "aws_iam_policy_attachment" "codebuild_lambda" {
  name       = "${var.env}-AWSLambdaFullAccess"
  policy_arn = "arn:aws:iam::aws:policy/AWSLambdaFullAccess"
  roles      = ["${aws_iam_role.codebuild.name}"]
}

resource "aws_iam_policy_attachment" "codebuild_api_gateway" {
  name       = "${var.env}-AmazonAPIGatewayAdministrator"
  policy_arn = "arn:aws:iam::aws:policy/AmazonAPIGatewayAdministrator"
  roles      = ["${aws_iam_role.codebuild.name}"]
}

resource "aws_iam_policy_attachment" "codebuild_iam" {
  name       = "${var.env}-AmazonAPIGatewayAdministrator"
  policy_arn = "arn:aws:iam::aws:policy/IAMFullAccess"
  roles      = ["${aws_iam_role.codebuild.name}"]
}

CodeBuildに関連付けるIAM Roleを設定しています。CodeBuildのコンテナ上でServerless Frameworkを実行させる必要があるので、それなりの権限が必要です。実質的にAdmin権限が必要なのですが、もう少し絞りたいなと思ったのでこねくり回しています。Serverless Frameworkを実行するホスト上で必要な権限についてはこちらのIssueでやり取りされているようですね。まだベストプラクティスのようなものはなさそうなので、今回は上記設定にしました。インラインで細かくポリシーを指定するのはシンドいので、マネージドポリシーに倒す方向にしています。

  • serverless-sample-app/buildspec.yml
version: 0.1

phases:
  install:
    commands:
      - npm install -g serverless
  pre_build:
    commands:
      - aws configure set s3.signature_version s3v4
      - aws s3 sync s3://$S3/${CODEBUILD_BUILD_ID%:*} .serverless
  build:
    commands:
      - sls deploy -s $STAGE

artifacts:
  files:
    - .serverless/*
  discard-paths: yes

2017年02月08日追記
.serverlessディレクトリ内に、CloudFormationテンプレートがなくても同じスタックに対してアップデートをしてくれるようです。そのため、必ずしもS3とのsyncが必要という訳ではありません。

ビルド時に実行するコマンドを定義しています。ポイントは以下の点です。

  • Serverless Frameworkは.serverlessディレクトリ上のCloudFormationテンプレートを元にスタックの作製/アップデートをするので pre_build フェーズでS3からテンプレートをsyncしている
  • CodeBuildは CODEBUILD_ という接頭辞を付けた各種環境変数を定義してくれているので、今回の例であれば CODEBUILD_BUILD_ID からプロジェクト名を取得している
    • 定義済み環境変数一覧はドキュメントに記載されてない?ようなので env で確認するといいかも
  • build フェーズでテンプレートのアップデート後、アーティファクト(Serverless Frameworkが生成したCloudFormationテンプレートなど)をS3にアップロードしている
    • 基本的にCloudFormationテンプレートだけ必要なのでServerless Frameworkが生成したデプロイメントパッケージは別にいらないかも
    • ソースコードさえあれば sls deploy -n で生成できるので
    • ただ、デバッグ目的であった方がいいと思う
  • テンプレートは上書き保存した方が管理しやすいので、S3のパスは常に同じになるようにしている

「Serverless Framework CodeBuild」などでググると、 buildspec.yml の設定方法がいろいろと出てきます。 sls deploy する前にテストなどを実行したいといった要件がある場合は以下のリンクが参考になりそうです。

もし手元でも動作確認したい場合は、以下のようにS3からテンプレートを取得すればOKです。

# リポジトリへ移動
$ cd path/to/repo
# sync
$ aws s3 sync s3://<_YOUR_S3_BUCKET_>/<_YOUR_PROJECT_NAME_> .serverless
# 確認
$ sls info -v

動作確認

いつものように plan / apply 後、AWS CLIでビルドを開始して意図した動作をするのか確認してみます。なお、そのままビルドを実行すると恐らく失敗すると思うので、下に記載している「利用する上での注意点」を参考にしてください。

  • プロジェクトが生成されたことを確認
$ aws codebuild list-projects \
  --region us-east-1
{
    "projects": [
        "tf-codebuild-demo-project"
    ]
}
  • プロジェクトの内容が意図したものになっていることを確認
$ aws codebuild batch-get-projects \
  --names "$(aws codebuild list-projects \
    --region us-east-1 \
    --query 'projects' \
    --output text)" \
  --region us-east-1
{
    "projectsNotFound": [],
    "projects": [
        {
            "name": "tf-codebuild-demo-project",
            "serviceRole": "arn:aws:iam::************:role/tf-codebuild-demo-codebuild-role",
            "tags": [
                {
                    "value": "tf-codebuild-demo-project",
                    "key": "Environment"
                }
            ],
            "artifacts": {
                "namespaceType": "NONE",
                "packaging": "NONE",
                "type": "S3",
                "location": "bcd1d3a6fa6d1004",
                "name": "tf-codebuild-demo-project"
            },
            "lastModified": 1486275186.92,
            "timeoutInMinutes": 60,
            "created": 1486275122.2,
            "environment": {
                "computeType": "BUILD_GENERAL1_SMALL",
                "image": "aws/codebuild/nodejs:7.0.0",
                "type": "LINUX_CONTAINER",
                "environmentVariables": [
                    {
                        "name": "S3",
                        "value": "bcd1d3a6fa6d1004"
                    },
                    {
                        "name": "STAGE",
                        "value": "dev"
                    }
                ]
            },
            "source": {
                "buildspec": "",
                "type": "GITHUB",
                "location": "https://github.com/knakayama/serverless-sample-app.git",
                "auth": {
                    "type": "OAUTH"
                }
            },
            "encryptionKey": "arn:aws:kms:us-east-1:************:alias/aws/s3",
            "arn": "arn:aws:codebuild:us-east-1:************:project/tf-codebuild-demo-project"
        }
    ]
}
  • ビルドの実行
$ aws codebuild start-build \
  --project-name "$(aws codebuild list-projects \
    --region us-east-1 \
    --query 'projects' \
    --output text)" \
  --region us-east-1
{
    "build": {
        "buildComplete": false,
        "initiator": "dev",
        "artifacts": {
            "location": "arn:aws:s3:::bcd1d3a6fa6d1004/tf-codebuild-demo-project"
        },
        "projectName": "tf-codebuild-demo-project",
        "timeoutInMinutes": 60,
        "buildStatus": "IN_PROGRESS",
        "environment": {
            "computeType": "BUILD_GENERAL1_SMALL",
            "image": "aws/codebuild/nodejs:7.0.0",
            "type": "LINUX_CONTAINER",
            "environmentVariables": [
                {
                    "name": "S3",
                    "value": "bcd1d3a6fa6d1004"
                },
                {
                    "name": "STAGE",
                    "value": "dev"
                }
            ]
        },
        "source": {
            "buildspec": "",
            "type": "GITHUB",
            "location": "https://github.com/knakayama/serverless-sample-app.git",
            "auth": {
                "type": "OAUTH"
            }
        },
        "currentPhase": "SUBMITTED",
        "startTime": 1486275509.793,
        "id": "tf-codebuild-demo-project:************************************",
        "arn": "arn:aws:codebuild:us-east-1:************:build/tf-codebuild-demo-project:************************************"
    }
}
  • ビルド結果の確認
$ aws codebuild batch-get-builds \
  --ids <_YOUR_BUILD_ID_> \
  --region us-east-1
{
    "buildsNotFound": [],
    "builds": [
        {
            "buildComplete": true,
            "phases": [
                {
                    "phaseStatus": "SUCCEEDED",
                    "endTime": 1486275517.655,
                    "phaseType": "SUBMITTED",
                    "durationInSeconds": 7,
                    "startTime": 1486275509.793
                },
                {
                    "contexts": [],
                    "phaseType": "PROVISIONING",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 65,
                    "startTime": 1486275517.655,
                    "endTime": 1486275583.106
                },
                {
                    "contexts": [],
                    "phaseType": "DOWNLOAD_SOURCE",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 9,
                    "startTime": 1486275583.106,
                    "endTime": 1486275592.316
                },
                {
                    "contexts": [],
                    "phaseType": "INSTALL",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 22,
                    "startTime": 1486275592.316,
                    "endTime": 1486275614.401
                },
                {
                    "contexts": [],
                    "phaseType": "PRE_BUILD",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 15,
                    "startTime": 1486275614.401,
                    "endTime": 1486275630.006
                },
                {
                    "contexts": [],
                    "phaseType": "BUILD",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 29,
                    "startTime": 1486275630.006,
                    "endTime": 1486275659.743
                },
                {
                    "contexts": [],
                    "phaseType": "POST_BUILD",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 0,
                    "startTime": 1486275659.743,
                    "endTime": 1486275659.791
                },
                {
                    "contexts": [],
                    "phaseType": "UPLOAD_ARTIFACTS",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 0,
                    "startTime": 1486275659.791,
                    "endTime": 1486275660.485
                },
                {
                    "contexts": [],
                    "phaseType": "FINALIZING",
                    "phaseStatus": "SUCCEEDED",
                    "durationInSeconds": 3,
                    "startTime": 1486275660.485,
                    "endTime": 1486275663.761
                },
                {
                    "phaseType": "COMPLETED",
                    "startTime": 1486275663.761
                }
            ],
            "logs": {
                "groupName": "****************************************",
                "deepLink": "https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#***************************************************************************************************",
                "streamName": "************************************"
            },
            "artifacts": {
                "location": "arn:aws:s3:::bcd1d3a6fa6d1004/tf-codebuild-demo-project"
            },
            "projectName": "tf-codebuild-demo-project",
            "timeoutInMinutes": 60,
            "initiator": "dev",
            "buildStatus": "SUCCEEDED",
            "environment": {
                "computeType": "BUILD_GENERAL1_SMALL",
                "image": "aws/codebuild/nodejs:7.0.0",
                "type": "LINUX_CONTAINER",
                "environmentVariables": [
                    {
                        "name": "S3",
                        "value": "bcd1d3a6fa6d1004"
                    },
                    {
                        "name": "STAGE",
                        "value": "dev"
                    }
                ]
            },
            "source": {
                "buildspec": "",
                "type": "GITHUB",
                "location": "https://github.com/knakayama/serverless-sample-app.git",
                "auth": {
                    "type": "OAUTH"
                }
            },
            "currentPhase": "COMPLETED",
            "startTime": 1486275509.793,
            "endTime": 1486275663.761,
            "id": "tf-codebuild-demo-project:************************************",
            "arn": "arn:aws:codebuild:us-east-1:************:build/tf-codebuild-demo-project:************************************"
        }
    ]
}

CloudFormationのアウトプットからAPI GatewayのURLを確認後、アクセスした際に以下のような出力が表示されたらOKです。

$ curl https://8ylep8hmwg.execute-api.us-east-1.amazonaws.com/dev -w '\n'
{"message": "Hello World at v0.1!"}

サンプルアプリのコードを修正後、再度ビルドしてみるとちゃんとアップデートされていることが確認できます。

$ curl https://8ylep8hmwg.execute-api.us-east-1.amazonaws.com/dev -w '\n'
{"message": "Hello World at v0.2!"}

利用する上での注意点

やはりまだ導入されてから日が経ってないという事情もあるので、いつくか利用する上で注意した方がよい点が散見されます。余裕があったらPR出して修正していきたいと思います。ただし、厳密に検証した訳ではなく、私の環境ではこういった事象が起きたという内容である点はご了承ください。

アップデート処理が微妙

新規リソースが追加された場合大抵このパターンになる傾向があるのですが、アップデート処理があまり安定していません。例えば、環境変数を追加しても「No changes. Infrastructure is up-to-date.」と言われてしまいます。

ソースリポジトリの認証処理が微妙

GitHubのリポジトリからソースを取得する際に、 authOAUTH を指定する必要があります。この設定をしないと DOWNLOAD_SOURCE のフェーズで以下のように処理に失敗します。

tf-codebuild-2

ただTerraformでこの設定をするとクラッシュします。。。しょうがないのでTerraformでプロジェクト作成後、マネジメントコンソールから手動でリコネクトさせています。しかし、この操作をした後に refresh などを実行すると再びクラッシュしてしまうので、Terraformの管理下から外さないといけなくなるのですが。。。以下のコマンドでリソースを削除可能です。

$ $GOPATH/bin/terraform state rm aws_codebuild_project.codebuild
Item removal successful.

試してないですが、tfstateファイルを直接修正すればマネジメントコンソール上での変更に追従できるかもしれません。

まとめ

いかがだったでしょうか。

TerraformとCodeBuildを利用したServerless Frameworkのデプロイ方法をご紹介しました。一度コードを書いておけばこれらサービスを素早く立ち上げられるというのは便利だと思います。今回は利用しなかったのですが、CodePipelineを設定すればリポジトリへのpushを契機としたデプロイフローの定義が可能です。TerraformもCodePipelineに対応してほしいですね。もちろんaws_cloudformation_stackを利用する手もありますけど。

本エントリがみなさんの参考になれば幸いに思います。