circleciとterraformで静的サイトのデプロイ

# はじめに
circleciとterraformでreactなどで作られた静的サイトをデプロイするまでの例です。

terraformでs3とcloudfrontの構築を行い、その後circleciでbuildした静的ファイルのs3へのアップロードを行います。

route53で独自ドメインからcloudfrontへのaliasレコード作るのはここではやってないです。cloudfrontのDistributionsを作ったらデフォルトで作ってくれる `*.cloudfront.net` のドメインで静的サイトが閲覧できるようにするまでがゴールです。

# terraform
## awsの設定
regionの指定、awsのproviderのverの固定、利用するcredentials(~/.aws/credentials)の指定をします。

```tf:aws.tf
provider "aws" {
  region  = "ap-northeast-1"
  profile = "private"
  version = "2.40.0"
}

```

## terraformの設定
利用するverの固定します。

```tf:terraform.tf
terraform {
  required_version = "0.12.6"
}

```

## s3の設定
バケットの作成です。

```tf:s3.tf
resource "aws_s3_bucket" "connpass-map-front" {
  bucket = "connpass-map-front"
  acl    = "private"
  region = "ap-northeast-1"
}

```

## cloudfrontとiamの設定
一旦全体のtfファイルを載せます。

```tf:cloudfront.tf
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
  comment = "origin access identity for s3"
}

resource "aws_cloudfront_distribution" "s3_distribution" {
  origin {
    domain_name = "${aws_s3_bucket.connpass-map-front.bucket_regional_domain_name}"
    origin_id   = "${aws_s3_bucket.connpass-map-front.id}"

    s3_origin_config {
      origin_access_identity = "${aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path}"
    }
  }

  enabled             = true
  is_ipv6_enabled     = false
  comment             = "connpass-map-front"
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "${aws_s3_bucket.connpass-map-front.id}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "allow-all"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

```

```tf:iam.tf
data "aws_iam_policy_document" "cf_to_s3_policy" {
  statement {
    actions = ["s3:GetObject"]

    resources = [
      "${aws_s3_bucket.connpass-map-front.arn}",
      "${aws_s3_bucket.connpass-map-front.arn}/*",
    ]

    principals {
      type        = "AWS"
      identifiers = ["${aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn}"]
    }
  }
}

resource "aws_s3_bucket_policy" "cf-to-s3" {
  bucket = "${aws_s3_bucket.connpass-map-front.id}"
  policy = "${data.aws_iam_policy_document.cf_to_s3_policy.json}"
}
```

### Origin Access Identityの設定
cloudfrontからのみ、s3にアクセスできるようにするため、Origin Access Identityの設定をしています。

まず、cloudfrontでorigin_access_identityのリソースを作成し、

```tf:cloudfront.tf
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
```

そのリソースが紐づいた形のポリシーを作成。

```tf:iam.tf
data "aws_iam_policy_document" "cf_to_s3_policy" {
```

作成したポリシーを作成したs3のバケットに紐付け。これでcloudfront経由でのみs3にアクセスできる状態になります。

```tf:iam.tf
resource "aws_s3_bucket_policy" "cf-to-s3" {
```

### cloudfrontのdistributionの作成

```tf:cloudfront.tf
resource "aws_cloudfront_distribution" "s3_distribution" {
```

distributionのリソースで、特筆するところとしては
s3のバケットを指定する箇所（originとか）は作成したバケットを指定するようにすること、 

```tf:cloudfront.tf
  origin {
    domain_name = "${aws_s3_bucket.connpass-map-front.bucket_regional_domain_name}"
    origin_id   = "${aws_s3_bucket.connpass-map-front.id}"
```


`default_root_object` にindex.htmlを指定すること、（buildしたファイル群にindex.htmlがある前提）

```tf:cloudfront.tf
  default_root_object = "index.html"
```

`viewer_certificate` は独自ドメインは利用しないのでデフォルトのままにすること

```tf:cloudfront.tf
  viewer_certificate {
    cloudfront_default_certificate = true
  }
```

これで `terraform apply` すれば、s3とcloudfrontが作成されます。

## tfstateの管理
`terraform apply` するとローカルにtfstateが作成されますが、tfstateはs3かterraform cloudで管理するのが定石になってます。（チームで開発するのであれば）

ここではterraform cloudを使ってtfstateの管理をやってみます。
流れとしては以下のような感じです。

1) [ブラウザ] terraform cloudでアカウント作成
2) [ブラウザ] organizationの作成
3) [ブラウザ] terraform cloudにアクセスするためのトークンの作成
4) [ローカル] 3で作成したトークンを $HOME/.terraformrc に書く
5) [ローカル] tfstateをterraform cloudに保存するように設定
6) [ブラウザ] workspaceの設定でawsの操作はローカルから行うように変更
7) [ローカル] terraform init

### 1 〜 3
1 〜 3までの流れは以下の記事に画像付きで載ってます。
https://dev.classmethod.jp/cloud/aws/manage-tfstate-terraform-cloud/

### 4) [ローカル] 3で作成したトークンを $HOME/terraformrc に書く

```tf:$HOME/.terraformrc
credentials "app.terraform.io" {
  token = "作成したトークンをここに"
}
```

### 5) [ローカル] tfstateをterraform cloudに保存するように設定
terraform.tfに設定を追加。

```tf:terraform.tf
terraform {
  required_version = "0.12.6"

  backend "remote" {
    organization = "connpass-map-front"

    workspaces {
      name = "connpass-map-front-workspace"
    }
  }
}

```

### 6) [ブラウザ] workspaceの設定でawsの操作はローカルから行うように変更
ローカルでterraformを動かしている場合、awsのcredentilasはローカルの `~/.aws/credentials` をみに行きます。terraform cloudを使う場合、そのawsの操作をterraform cloudから行うか、ローカルから行うか設定できます。(workspaceの設定)

<img width="788" alt="スクリーンショット 2019-12-01 13.14.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/6cf58455-f1c0-8b48-c91e-fd8bbbea7119.png">

ここがremote（デフォルト値）となっていると、 `terraform apply` や `terraform plan` を実行した際、ローカルの `~/.aws/credentials` を見にいかなくなるので以下のエラーが発生します。

```
Error: No valid credential sources found for AWS Provider.
        Please see https://terraform.io/docs/providers/aws/index.html for more information on
        providing credentials for the AWS Provider

```

Localに設定すればawsの操作はローカルから実行し、tfstateの管理のみをterraform cloudが行うようになるので上記のエラーは出なくなります。

### 7) [ローカル] terraform init
terraform cloudにアクセスできたらこんな感じでinitが成功します。
これで `terraform apply` すればtfstateがterraform cloudにあがっていく状態になりました。

```
$ terraform init

Initializing the backend...

Successfully configured the backend "remote"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.40.0...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

ここまでで、環境は整いましたが、肝心のs3のバケットには何も入っていないので、バンドルされた静的ファイルをs3にアップロードする流れをcircleciで設定します。

# circle ci
`npm run build` でビルドしたファイルを/buildに配置するようになっている前提です。

```yml:.circleci/config.yml
version: 2.1
orbs:
  aws-s3: circleci/aws-s3@1.0.11
executors:
  default:
    docker:
      - image: circleci/node:12
    working_directory: ~/repo
commands:
  npm_install:
    steps:
      - restore_cache:
          keys:
            - v2-dependencies-{{ checksum "package.json" }}
            - v2-dependencies-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: v2-dependencies-{{ checksum "package.json" }}
jobs:
  build:
    executor: default
    steps:
      - checkout
      - npm_install
      - run: npm run build
      - persist_to_workspace:
          root: .
          paths:
            - "*"
            - ".*"
  deploy:
    executor: default
    steps:
      - attach_workspace:
          at: ~/repo
      - aws-s3/sync:
          from: build
          to: s3://connpass-map-front
          overwrite: true
workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - /feature.*/
                - master

```

[aws-s3のorbs](https://circleci.com/orbs/registry/orb/circleci/aws-s3)を使っているので、これでconnpass-map-frontのバケットにbuild配下のファイルがsyncされます。

```yml:.circleci/config.yml
      - aws-s3/sync:
          from: build
          to: s3://connpass-map-front
          overwrite: true
```
また、このs3のorbsを使うためには、事前にcircleciの環境変数にAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_REGIONを設定しておく必要があります。

このcircleciの設定で、masterにブランチがマージされる度にs3へ最新のバンドルファイルがアップロードされます。

アップロードが終わったあとで、作成したcloudfrontのDistributionsのドメイン(*.cloudfront.net)にアクセスすればアップロードした静的なサイトが閲覧できます。

# 参考
https://qiita.com/natsumisawa/items/404217208ab1c96d8719
https://dev.classmethod.jp/cloud/aws/manage-tfstate-terraform-cloud/