LP公開のためのインフラをTerraform + AWSでささっと？構築する（ソース付き）

# はじめに

独立起業している知人（建築系なので、以下👷‍♂️）から、自社で直接仕事の依頼を受けるためのランディングページを作りたい...🔥という相談を受けました

[MVP](https://uxmilk.jp/65654)の考え方からいけば、[こういうの](https://portfolio-ai.com/instapage_introduce)を使ったり、作るにしてもherokuなどを使ったりでもよかったのかもしれませんが、
業務で触れていなかったRoute53やCloudfrontなどのサービスを触る格好のチャンスに思えたので、AWSのアカウントを新規で作って1から構築することとしました

もっとも自分で作ると言っても、コンソールから手でポチポチやって後々に🤗ってなるのは嫌だったので、
以前勧めてもらって気になっていた[Terraform](https://qiita.com/Chanmoro/items/55bf0da3aaf37dc26f73)を勉強して、LP公開+ちょっとした処理をするためのAPI...という一式を作成しようと決めました
（職場のインフラはあまり[IaC](https://qiita.com/KeijiYONEDA/items/e9ed0daf0906adec810e)を意識したものになっていないので、業務に還元できないかな...という思いも少しあります🤔）

**ささっと**とか虫のいいことを言っています🐞が、
作ってる最中は己のDNSやhttpsなどに対する基礎知識の足りなさから、もうだいぶ苦しみました⚰
ので、そこら辺の備忘録も兼ねて、紹介していきたいと思います

*注意：理由は後述しますが、環境構築には状況により数日かかります...ちっともささっとじゃない！*

# どんなものを作るか

👷‍♂️と対面やGoogleハングアウトで会議を重ねて、大体以下のような話が出てきました

- LP制作は外注しようね
    - 👷‍♂️がLPのテキストや構成を考えることに意欲的で、割とちゃんとした仕様を作ってくれて大いに助かる
    - デザイン・コーディングまでやって頂けるフリーランスの方に[ランサーズ](https://www.lancers.jp/)にて依頼
- インフラと問い合わせを受ける仕組みをぼくが作るね
    - 問い合わせの内容は、👷‍♂️との会話用に作ったSlackで受けとることに
    - 副次的に出てきた要件
        - APIトークンのハードコードは嫌だよね
        - 問い合わせフォームあるしhttps必須だね
- もちろん独自ドメイン取りたいよね
    - Route 53で見てみたら.jpドメインが受け入れ難い金額💸だったため、レジストラは[お名前.com](https://www.onamae.com/)、ネームサーバはRoute 53を利用
    - 当初ここら辺の概念を全く分かってなくて時間が溶けました。[こちら](https://www.onamae.com/clever/about/regist.html)や[こちら](https://www.atmarkit.co.jp/ait/articles/0112/27/news001.html)の記事がオススメです
- SSL証明書の更新は自動化したいよね
    - 個人的にタイムリーな話だったため
    - ここも何も分かってないことに気づき[こちら](https://milestone-of-se.nesuke.com/sv-advanced/digicert/digital-certification-summary/)など読みました

# どのように作るか

ということで、以下のようなインフラを構築🏗することにしました！

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/d10aff6c-8454-4aaa-023c-b41536f7b488.png)

簡単にですが一通り説明します
本番運用するものはLPや問い合わせ機能はインフラとは別で管理したいですが、本記事では簡便のため全部まとめてTerraformで作成します

|名称|役割|
|---|---|
|お名前.com|ドメインを取得・管理。Terraform管理外|
|Route 53|ネームサーバ(Hosted Zone)いっこ作る|
|Certificate Manager(⭐️)|SSL証明書の生成・管理。更新自動化のため[DNS検証](https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/gs-acm-validate-dns.html)を利用|
|Secrets Manager(🔐)|[Slack API](https://api.slack.com/)のトークンを管理|
|CloudFront|LPを公開。特定のパス配下をAPIへ回す。[地域制限](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/georestrictions.html)を使って日本(と米国)に絞る|
|API Gateway|問い合わせAPIのエンドポイント。[リージョンAPI](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/create-regional-api.html)として構築|
|S3|LPの置き場所と、[CloudFrontのアクセスログ](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html)保管先|
|Lambda|問い合わせをSlackに投稿|
|Slack|チャンネルに問い合わせが上がってくる。Terraform管理外|

書いてて気づいたんですが、これそのままSPAの公開なんかにも応用できそうな構成ですね😎

# 作ったもの

[GitHub](https://github.com/yktakaha4/lp-infra-by-terraform)を見てください:octocat:
だけだとあんまりなので、 順を追って説明していきます

## 準備

Terraformの前にいくつか必要なものがあります
具体的な作り方は記事の本旨から外れるため、各自おググりください🙄

### お名前.comでドメインを取得

他のレジストラでもいいものと思います
Route 53を使う場合は、ネームサーバへの登録含め自動化できるんでしょうか？
（.jpが高いからとか言ってた割に.comなのはサンプルだからです）

![ドメイン名](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/6c3219cf-aefd-1209-3c87-d75a56c32df7.png)

### Slackで投稿用Botを作成

Slack APIは[chat.postMessage](https://api.slack.com/methods/chat.postMessage)を使います
所定の権限を付与したbotを作成した上で、OAuth Access Tokenをメモっておきましょう

![Access Token](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/7b608760-fd3c-5b97-306a-ffdb2ea39bae.png)

また、投稿先のチャンネルIDも必要になりますので、`https://xxxxx.slack.com/messages/CGxxxxxxx`の`CGxxxxxxx`の部分も確認しておいてください


### AWSアカウントとユーザ

`PowerUserAccess`がついたユーザの`AWS_ACCESS_KEY_ID`および`AWS_SECRET_ACCESS_KEY`が必要です
あと、作っただけでたくさんお金のかかるものは定義していないつもりですが、万が一に備えて請求ダッシュボードの設定やアラームは設定して頂いておいた方がよいでしょう
請求については自己責任でお願いします🙏

## いざ構築

準備ができたら、実際にTerraformにて環境を構築していきます！

の前に、以下をインストールしておいてください
具体的な方法、およびWindowsの方は🙄

- [direnv](https://qiita.com/kompiro/items/5fc46089247a56243a62)
- [tfenv](https://qiita.com/kamatama_41/items/ba59a070d8389aab7694)
    - terraformのインストールもお願いします
    - 本稿では`v0.12.3`を使用しています

```shell_session:ターミナル
$ direnv version
2.20.1

$ tfenv -v
tfenv 1.0.1

$ terraform -v
Terraform v0.12.3
```

それではやっていきましょう
なにはともあれ、まずはリポジトリを取得します

```shell_session:ターミナル
$ git clone https://github.com/yktakaha4/lp-infra-by-terraform.git

$ cd lp-infra-by-terraform
$ ls -l
```

次に、各種設定ファイルを作っていきます
`.sample`とついているやつがサンプルなので、コピーの上値を埋めてください
`AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY`、`domain_name`は準備の項で用意した値、
`resource_prefix`はS3のバケット名など各種リソースに設定されるので、人と被らなさそうなものを任意で設定してください

なお、`terraform.tf.sample`は、tfstateファイルをS3で管理する場合必要になります
お試しでやるようでしたら特に不要です

```shell_session:ターミナル
$ cp -p terraform.tfvars.sample terraform.tfvars
$ vim terraform.tfvars

$ cp -p .envrc.sample .envrc
$ vim .envrc
$ direnv allow

# 複数人で共有して開発する場合のみ設定
$ cp -p terraform.tf.sample terraform.tf
$ vim terraform.tf
```

ここまで険しい道のり⛰でしたが、いよいよ`terraform`コマンドを使っていきます

Terraformは、カレントディレクトリ直下にあるtfファイルを参照して各種リソースを作成します
`plan`で作成されるリソースの確認、`apply`で適用です。簡単ですね！

今回はドメインの取得をお名前.comで行なっていますので、
Route 53でホストゾーンを作成した後に、ネームサーバのアドレスをお名前.comの管理コンソールに設定する必要があります
こちらは手作業で行う必要があるため、まずホストゾーンのみ作成するコマンドを実行します

`-target=xxxx`で、特定のリソースのみ作成することができます
今回は以下のリソースのみ作成します

```hcl:route53.tf
resource "aws_route53_zone" "domain_name" {
  name = "${var.domain_name}"
}
```

それでは、いってみましょう！

```shell_session:ターミナル
# 環境構築
$ terraform init

# お名前.comにネームサーバーの情報を登録する必要があるため、まずホストゾーンのみ作成する
$ terraform plan -target=aws_route53_zone.domain_name
$ terraform apply -target=aws_route53_zone.domain_name

Outputs:

name_servers = [
  "ns-xxxx.awsdns-xx.org",
  "ns-xxxx.awsdns-xx.co.uk",
  "ns-xxxx.awsdns-xx.com",
  "ns-xxxx.awsdns-xx.net",
]
```

Outputsとして、上記のようにネームサーバーのドメインが表示されたら無事成功です
AWSのコンソールから`Route 53 -> ホストゾーン`と辿っていくとドメインができてるはずですので、見てみてください

![ホストゾーン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/02ae7df3-89a6-29d8-212e-6a00a8056df2.png)

いい感じですね！

ここで、Terraformを一度離れて、ネームサーバーをお名前.comへ登録します
[こちら](https://www.onamae.com/guide/p/67)に具体的な手順が書いてありますので、参考にしつつやってみてください

![ドメインNavi](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/1e286efa-0f66-85d6-c6c6-10772a3fa6c1.png)

できましたでしょうか？

お名前.comのコンソールからの登録自体は割とすぐに終わるのですが、
設定したネームサーバが浸透（という言い方でいいんでしょうか？）するには、[特に再作成時は最大で72時間程度かかる](https://faq.nttpc.co.jp/faq/show/13352?site_domain=suitex)そうです😕
ネームサーバの変更が完了していないと以降のコマンドを打ってもタイムアウトしますので、ここはグッとこらえて時が経つのを待ちます...

`dig`コマンドを使うと、ドメイン名に対応するネームサーバを確認することができるので、時間を開けて見てみてください

```shell_session:ターミナル
# ネームサーバの確認用コマンド
$ dig example-domain.com ns

;; ANSWER SECTION:
xxxxxxxxx.com.          172756  IN      NS      ns-xxxx.awsdns-xx.net.
xxxxxxxxx.com.          172756  IN      NS      ns-xxxx.awsdns-xx.org.
xxxxxxxxx.com.          172756  IN      NS      ns-xxxx.awsdns-xx.co.uk.
xxxxxxxxx.com.          172756  IN      NS      ns-xxxx.awsdns-xx.com.

# ↑で返却される内容が、 name_servers で表示されたネームサーバに問い合わせた時と同じだったら準備完了
$ dig example-domain.com @ns-xxxx.awsdns-xx.org ns
```

いずれにせよすぐには終わらないので、もういくつか先に済ませておいた方がいい内容をやっておきます

Slack APIから取得したトークンとチャンネルIDをSecrets Managerに登録します

```shell_session:ターミナル
$ terraform plan -target=aws_secretsmanager_secret_version.slack_api
$ terraform apply -target=aws_secretsmanager_secret_version.slack_api
```

applyが完了したらAWSコンソールを開いて、それぞれのシークレットの値を編集してください
本来、Terraformで作ったリソースに手で変更を加えると差分検知されるような仕組みになっているのですが、
このシークレットの値については差分検知の確認対象外にしています

![Secrets Manager 2019-07-08 22-47-30(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/0f7e6687-9b73-1f13-64a3-fd9d4d8dfbc0.png)

そうこうしているうちにDNSの浸透が済んだら、あとは一気に作成できます✌️

`-target`の指定を外すと全てのリソースを作成できるので、一思いにやってしまいましょう
CloudFrontなど作成に時間のかかるリソースも色々あるので、1時間ほどは見ていただいた方がいいようです

```shell_session:ターミナル
# 残りのリソースを適用
$ terraform plan
$ terraform apply
```

...無事に正常終了しましたでしょうか？

もしもうまくいったようであれば、しばし待ったのちご自身の`https://example-domain.com/`にアクセスすると、以下のようなページが表示されるはずです
(なお、htmlは[Paper Kit 2](https://demos.creative-tim.com/paper-kit-2/index.html)を切り貼りしてでっち上げたものです)

![LP](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/efcb4020-637b-b0b7-4d9c-d129cb635486.png)

おもむろにボタンも押してみましょう...！

![Slack](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/49e67f8e-8212-5d3d-0a1d-e4d52953f30f.png)

これにてガンガン仕事を受けられますね！！！🍾🎂🎉

## トラブルシューティング(構築)

私が引っかかったものを晒しておきます
以下以外のものが出てきた場合は、コメントで教えて頂けたら補記したいと思います
（こうすれば解決できた...というところまでやっていただけるとありがたいです）

```shell_session:その1
# CloudFrontでエラーの場合は、AWSコンソールを確認し、ステータスが Deployed になるまで待つ
Error: error waiting until CloudFront Distribution (xxxxx) is deployed: timeout while waiting for state to become 'Deployed' (last state: 'InProgress', timeout: 1h10m0s)

# Deployed になった後、 untaintの実行
$ terraform untaint aws_cloudfront_distribution.domain_name
Resource instance aws_cloudfront_distribution.domain_name has been successfully untainted.

# 残りのリソースを適用
$ terraform apply
```

```shell_session:その2
# ACMで発行できる証明書の数は1年間で上限があり、作成・削除を繰り返す等して最大値を越えるとエラーとなってしまう
Error: Error requesting certificate: LimitExceededException: Error: you have reached your limit of 10 certificates in the last year.

# サポートに投げると上限緩和してもらえる
# https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/acm-limits.html#limit-certs-yearly
```

# 削除方法

作るのに引き換え削除は楽チンです。`terraform destroy`で削除できます

ただし何も指定しないと本当に全部消えます👋
planをよく確認したり、`target=xxxxx`で部分的に消していくなど、慎重に作業した方がよいでしょう

```shell_session:コンソール
$ terraform plan --destory
$ terraform destroy
```

## トラブルシューティング(削除)

作るときと同様、引っかかったものを書いておきます

```shell_session:その1
# 以下エラーの場合は、AWSのコンソールなどから対象のバケットの中身を空にする
Error: error deleting S3 Bucket (xxxxx): BucketNotEmpty: The bucket you tried to delete is not empty
```

# 所感

環境構築を手作業で行なっていると、いつも「これで確認すべき項目は十分だろうか...？」とか「設定値正しく設定できてるかな...？」とか、
よしんば動いたとしても「本当にあれでよかったんだろうか...🤢」という気持ちが付きまとっていた（しかも時間が経つと全部忘れる）のですが、
インフラをコードに落とし込めると、アプリケーションエンジニアにとってもグッと距離感が近づいたような気がしました
特に、**リソース間の依存関係を理解する**という意味では、GUIよりもコードの方が優れているように感じました

また、個人開発だと、寝る前のちょっとした時間で開発したり、前回の作業から間が空いてしまったり...ということもよくあると思いますが、
どこまで何を作ったか、tfファイルやtfstateファイルにしっかり残るので、**安心してやったことを忘れることができる**というのが地味にありがたかったです😋

今回の記事では、作成したtfファイルについてはほとんど触れませんでしたが、
各ファイルの設定値とAWSコンソールの状態を比較したり、内容を追加・削除してみたりと、ぜひ各自で色々試して頂くことをお勧めします！
（よくないところがあったら、プルリクやコメントで指摘頂ければ幸いです）

自分が書いたtfファイルで次々にインフラが組み上がっていく様を見ていると、
それこそ[創造主になったような気持ち🌏](https://ja.wikipedia.org/wiki/%E3%83%86%E3%83%A9%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9F%E3%83%B3%E3%82%B0)になれますよ！

# 参考資料

オススメ情報のリンクを貼っておきます📚

- [VSCodeでTerraformを書くときの設定](https://qiita.com/pypypyo14/items/5520f3defa55119f3a1a)
    - 私はVSCodeで開発していますが、Intellijなんかもいい感じに使えるらしいです
- [【ダウンロード版】Pragmatic Terraform on AWS](https://booth.pm/ja/items/1318735)
    - ともあれこちらを読むことをお勧めします
    - 今回使わなかったサービス含め、様々なリソースの作り方について具体的に書いてあります
- [Terraformの公式ドキュメント](https://www.terraform.io/docs/providers/aws/)
    - 設定値など分からないことがあったら、`resource "aws_xxxxx"`などのリソース名で検索して↑のサイトを見れば基本全部書いてあります
- [Terraform職人入門: 日々の運用で学んだ知見を淡々とまとめる](https://qiita.com/minamijoyo/items/1f57c62bed781ab8f4d7)
    - tips集です。記法や考え方が頭に馴染んできたら読むのをお勧めします