Open20

インフラをTerraformで一元管理する

LeivyLeivy

Terraform とは

Terraform とは、HashiCorp社が提供しているオープンソースの「Infrastructure as Code (IaC)」ツールです。インフラストラクチャ(サーバー、ネットワーク、ストレージ、DNSなど)をコードとして記述し、自動化と再現性を持って管理・デプロイすることができます。

https://www.terraform.io/

LeivyLeivy

開発環境の構築

今回は WSL2 + Ubuntu + DevContainer + Docker を使用して、開発環境を構築します。
Dockerイメージは公式のものがあるようなので、それを使用します。
Terraformのバージョンは最新のものを使用します。

最新のバージョンは以下から確認しましょう。

https://releases.hashicorp.com/terraform/

LeivyLeivy

AWS CLI v2とHashiCorp Terraform公式Dockerイメージの相性問題

AWS CLI v2はglibc環境が必要ですが、AlpineベースのTerraform公式Dockerイメージでは適切に構築できず、多くの課題がありました。その際に以下の記事を発見しました。

該当Issue


結論

Terraform公式イメージで環境を構築しようと試みましたが、AlpineベースのOSではglibcの依存関係などの問題が多く、最終的にDebian SlimUbuntuの選択に至りました。

TerraformやAWS CLIの使用頻度と公式ガイドの採用例を考慮した結果、Ubuntu 22.04を採用して環境を構築することにしました。

以下はそれぞれのファイルです。

Dockerfile

FROM ubuntu:22.04

# 環境変数
ENV LANG=ja_JP.UTF-8 \
    TF_VERSION=1.9.8 \
    AWS_CLI_URL=https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \
    HADOLINT_VERSION=v2.12.0

# システムのアップデートと基本ツールのインストール
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y --no-install-recommends \
        less \
        vim \
        curl \
        unzip \
        git \
        make \
        locales \
        ca-certificates \
        openssh-client && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# ロケールの設定
RUN locale-gen ${LANG} en_US.UTF-8 && \
    update-locale LANG=${LANG} && \
    update-ca-certificates

# AWS CLI v2 のインストール
WORKDIR /tmp/awscli
RUN curl --silent --show-error --fail "$AWS_CLI_URL" -o "awscliv2.zip" && \
    unzip -q awscliv2.zip && \
    ./aws/install && \
    rm -rf awscliv2.zip aws

# Terraform のインストール
WORKDIR /tmp/terraform
RUN curl --silent --show-error --fail -L "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" -o "terraform.zip" && \
    unzip -q -d /usr/local/bin/ terraform.zip && \
    rm terraform.zip

# git-secrets のインストール
WORKDIR /tmp/git-secrets
RUN git clone https://github.com/awslabs/git-secrets.git . && \
    make install && \
    rm -rf /tmp/git-secrets

# hadolint のインストール
WORKDIR /tmp/hadolint
RUN curl --silent --show-error --fail -L "https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-x86_64" -o /usr/local/bin/hadolint && \
    chmod +x /usr/local/bin/hadolint

# vscode ユーザー作成と初期設定
WORKDIR /workspace
RUN adduser --disabled-password --gecos "" vscode && \
    mkdir -p /home/vscode/.aws /workspace && \
    chown -R vscode:vscode /home/vscode /workspace

# 初期化スクリプトのコピー
COPY .docker/terraform/init.sh /usr/local/bin/init.sh
RUN chmod +x /usr/local/bin/init.sh

# 作業ディレクトリ設定
USER vscode
WORKDIR /workspace

# エントリーポイントの設定
ENTRYPOINT ["/usr/local/bin/init.sh"]

compose.yml

services:
    terraform:
        container_name: 'terraform'
        image: local/terraform
        build:
            context: .
            dockerfile: .docker/terraform/Dockerfile
        volumes:
            - .:/workspace
            - ./.docker/terraform/.aws/config.default:/home/vscode/.aws/config
            - ./.docker/terraform/.aws/credentials.default:/home/vscode/.aws/credentials
        environment:
            - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
            - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
        tty: true

init.sh

#!/bin/bash

# AWS Config の設定
if [[ ! -f /home/vscode/.aws/config ]]; then
    mv /home/vscode/.aws/config.default /home/vscode/.aws/config
fi

# AWS Credentials の設定
if [[ ! -f /home/vscode/.aws/credentials ]]; then
    mv /home/vscode/.aws/credentials.default /home/vscode/.aws/credentials
    sed -i "s/<access-key>/\${AWS_ACCESS_KEY_ID}/g" /home/vscode/.aws/credentials
    sed -i "s/<secret-key>/\${AWS_SECRET_ACCESS_KEY}/g" /home/vscode/.aws/credentials
fi

# シェルを起動
exec bash
LeivyLeivy

一括インポートできない

次のエラーが発生し、一括インポートができない問題に直面しました

aws error initializing resources in service accessanalyzer, err: operation error AccessAnalyzer: ListAnalyzers, failed to resolve service endpoint, an AWS region is required, but was not found

https://github.com/GoogleCloudPlatform/terraformer/issues/1590

そこで次のようにしてみたところインポート自体は成功した

$ terraformer import aws -r="ec2_instance,s3" --regions=ap-northeast-1

リージョンを明記するのも大事そう(明記しなかった場合s3が正しくインポートできなかった)

結論

いくつかのリソースで同時にインポートしようとするとエラーを吐くものがあったので、ひとつずつインポートすることにしました。

https://github.com/GoogleCloudPlatform/terraformer/blob/master/docs/aws.md

ただ、Cloud Mapのリソースがうまく取得できなかったので、これはterraform importコマンドでインポートしました

LeivyLeivy

インポートしたリソースの整理

以下を参考に整理していきます

https://qiita.com/YK0214/items/9e5975cc3cb50192719e

調査を進めると、どうやらterrraformerは銀の弾丸ではなかった模様・・・
結局インポートしたリソースを"使って"構築するという認識が正しいのかもしれない
そのままでは使えないですね

https://qiita.com/Kiyo_Karl2/items/0ef55620dbe507a793ac
https://developer.hashicorp.com/terraform/language/v1.1.x/upgrade-guides/1-0

さらに、terraformerでインポートすると Terraform 0.12 環境の tfstate になるため、Terraform 1.0 までアップグレードする必要があるようです

https://developers.cyberagent.co.jp/blog/archives/33331/

まずはインポートしたすべてのリソースのバージョンを、最新の1.9.8にアップグレードしていきます

tfenv を利用し適宜 Terraform のバージョンを変更していますが、下位ディレクトリに .terraform-version ファイルがある場合にはそのバージョンに引っ張られるため、.terraform-version を変更し、作業前に意図したバージョンになっているかを $ terraform version で確認することをオススメします。

0.12 -> 0.13

$ cd generated/aws/ec2_instance
$ tfenv use 0.13.7
$ terraform 0.13upgrade
$ terraform init
$ terraform plan
$ terraform apply

Warning で非推奨の書き方をしている場合があるのでそれを修正しましょう。

Warning: Interpolation-only expressions are deprecated
on outputs.tf line 6, in output "aws_instance_tfer--i-06ab3a2a9cffebacd_hoge_id":
6: value = "${aws_instance.tfer--i-06ab3a2a9cffebacd_hoge.id}"
これは
value = aws_instance.tfer--i-06ab3a2a9cffebacd_hoge.id
と書き換えるだけです。

なお、このエラーの修正作業には以下のGPTs等を活用しました
https://chatgpt.com/g/g-BGOjJYEsy-noterraform-expert

0.13 -> 0.14

$ tfenv use 0.14.11
$ terraform init
$ terraform plan
$ terraform apply

0.14 -> 1.9.8

$ tfenv use 1.9.8
$ terraform init
$ terraform plan
$ terraform apply

これで 0.12 から最新のバージョンへアップグレードできます。
ローカルにある tfstate の terraform_version が 1.9.8 になっているはずです。

続けて、以下を参考にリソースを一つずつterraformで構築していきます

https://engineering.nifty.co.jp/blog/29177
https://speakerdeck.com/harukasakihara/besutona-terraform-deirekutorigou-cheng-wokao-cha-sitemita?slide=6
https://techblog.forgevision.com/entry/Terraform/directory

LeivyLeivy

Terraformで既存のAWSリソースをインポート

次に、terraformerでインポートしたリソースを、各プロダクト名ごとに分割したディレクトリで再構築する作業を進めました。
再利用性を考慮し、利便性が高いリソースについてはモジュール化を行いながら、以下のコマンドを繰り返してリソースのインポートを実施しました。

インポートのコマンド例

Terraformで既存リソースを管理対象に追加するには、以下のようなコマンドを使用します。

モジュールの場合

terraform import module.<モジュール名>.<対応するAWSのリソース名>.this <引数に対応する値>

モジュールを使用しない場合

terraform import <対応するAWSのリソース名>.<terraformで管理する名前> <引数に対応する値>

ポイント

  • <モジュール名>:再利用を目的としたモジュール名を指定。
  • <対応するAWSのリソース名>:AWSリソース(例: aws_instance, aws_s3_bucketなど)を指定。
  • <terraformで管理する名前>:Terraform内でリソースを識別する名前。
  • <引数に対応する値>:AWSリソースのIDやARNなど、インポート対象を一意に識別する値。

terraform initplanapplyの実行

インポート後、terraform initでTerraform環境を初期化し、terraform planで状態の確認を行いました。エラーがないことを確認したらterraform applyを実行して、管理対象に追加しました。

LeivyLeivy

AWS Organizationsの導入

環境ごとにディレクトリを切って管理する方法を採用しましたが、AWSのベストプラクティスとしては環境ごとにAWSアカウントもわけたほうがいいそうです

https://x.com/engineer_ryoma/status/1866610634554741231

というわけでAWS Organizationsを導入し、terraform上でそれらも管理できるようにします

https://cloudnavi.nhn-techorus.com/archives/3977

ベストプラクティスについてもしっかり確認します

https://aws.amazon.com/jp/blogs/news/best-practices-for-organizational-units-with-aws-organizations/

https://zenn.dev/pageo/articles/1f034563a917e3

LeivyLeivy

AWS SSO

AWS Organizationsで複数のアカウントを管理する場合、クレデンシャル管理が煩雑になるためAWS SSOも導入する必要がでてきました

AWS SSOとは
AWS Single Sign-On の略です。AWSの複数アカウントやアプリケーションへのアクセス管理を容易にし、1か所からSSOアクセスを提供するサービスです。

https://www.openupitengineer.co.jp/column/it-technology/8299

グループ設計について

スタートアップなど少人数での開発を前提とした企業やPJならまだしも、大規模開発/大企業/ミッションクリティカルなPJなどでIAM Policyを適当に設計するのはセキュリティ上かなり危険です。
セキュリティ(情報漏えい)事故の70%以上は内部犯行[1]と言われている昨今では、PJに参加した管理者/開発者/監査人含めて全ての人間が不正を起こす可能性を持っているという性悪説を前提に、権限管理を徹底すべきです
そのため、アクセス権限セットの設定では、PoLP: Principle of Least Privilege(最小権限/最小特権の原則)を必ず意識しましょう。
PJの初期段階でユーザー/グループを細かく設計し、役割ごとにアクセス権限セットのIAM Policyを適切なものに設定すべきです。

https://www.openupitengineer.co.jp/column/it-technology/8299

terraformで作る場合

https://dev.classmethod.jp/articles/aws-organizations-with-terraform/

マスターアカウントにログインした後に、新たに作成したAWSアカウントに切り替える

https://storage.googleapis.com/zenn-user-upload/057301120e28-20241213.png

LeivyLeivy

AWS IAM Identity Center

昔はAWS SSOと呼ばれていたました
https://zenn.dev/yaasita/articles/aa48bfa06b9a53

情報が少し古かったようです、AWS IAM Identity Centerについて調査をしました

https://zenn.dev/murakami_koki/articles/79ac2456564b36

https://qiita.com/tech4anyone/items/78bafc1ca6fb4b7bc1d2?utm_source=chatgpt.com

有効化

ここはterraformではなくマネジメントコンソール上から有効化する必要があるみたいです

terraformで設定

https://dev.classmethod.jp/articles/iic-setting-terraform/

https://zenn.dev/nosuid/articles/b32ab6870afb18?utm_source=chatgpt.com#許可セットについて

LeivyLeivy

AWS SSOでTerraformを使う

aws configure sso

ログインすると以下が更新される

~/.aws/config

terraformの設定

export AWS_PROFILE="<使用したいプロファイル名>"

terraform plan

この場合、main.tfではプロファイルの指定をしません

backendの設定

terraform init -backend-config="profile=<SSOプロファイル名>"

https://zenn.dev/quo1987/scraps/063eeb33a553a9

LeivyLeivy

AWS Control Towerの設定

IAM Identity Centerで各環境用アカウントを作成し、各環境用のtfstate保存バケットを作成したタイミングでtfsecによりセキュリティの問題があることを指摘されました
その中で、バケットのロギング用バケットを指定する必要があるのですが、これを一元管理するためにAWS Control Towerというサービスも必要になりました

https://docs.aws.amazon.com/ja_jp/controltower/latest/userguide/about-logging.html?utm_source=chatgpt.com
https://dev.classmethod.jp/articles/setup-landing-zone-using-terraform/
https://note.com/ulsystems/n/n9515440f6043#26dfa3a7-8c56-4563-a81e-20290895c09c
https://pages.awscloud.com/rs/112-TZM-766/images/20230831_33th_ISV_DiveDeepSeminar_hennge.pdf

上記の記事を参考にし、ひとまずAuditアカウントとLogArchiveアカウントを作成し、SecurityOUに集約しました

terraform上でランディングゾーンを作成しようとするとバグが発生する未解決の問題があるため、結局ランディングゾーンは手動で作成しました。
最初に作成したアカウントは自動作成されるOUに再度アタッチすることになったので、最初から手動で作成し、あとからterraform importする方がよかったです。

手動で作成を試みたところ以下のエラーに遭遇しました

ドキュメントなどによると、Organizationsのサービスの設定で有効化されている以下の二つを無効化する必要がありました


https://dev.classmethod.jp/articles/pre-check-of-control-tower-config/

Configだけ無効化してもいけるようです

https://us-east-1.console.aws.amazon.com/organizations/v2/home/services

以下を参考に進めます

https://www.ctc-g.co.jp/solutions/cloud/column/article/33.html

成功し、最終的には以下のような構成になりました
Control Towerのサイドメニュー内の「組織」という項目から、すでにOrganizationsで作成済みのアカウントをControl Towerに登録していきます

登録を進めていると以下のエラーに遭遇しました

Management の登録に失敗しました。

AWS Control Tower detected errors that prevent the baseline version from activating on your OU. To continue, navigate to the OU detail page in the console and download a list of items to remediate.


どうやらrootアカウントではこの操作ができないようです
ルートではないアカウントで操作します
マルチアカウントでドメインはどこで持つの問題についても挙げられています

マルチアカウントでのドメインを所持する場所ですが、
各システムの本番環境でドメインを取得するのがよいです。
理由として

  • 他の担当者が他のシステムのドメインをいじれる事の防止
  • controll towerタワーの親アカウントは、権限付与と請求関係とする役割の分離
  • システムを開発会社が変わり移譲する場合も各システム本番で持っていた方が楽(親会社から子会社)
    という事が上げられます。
    https://syoblog.com/aws-account/

以下を参考に、Control Towerを操作するIAMユーザーに「Service Catalog」内のポートフォリオへのアクセス権限をマネジメントコンソール上から与えます

https://dev.classmethod.jp/articles/resolve-no-launch-paths-found-for-resource-on-control-tower/

無事に成功しました

LeivyLeivy

S3のクロスアカウントレプリケーション

無事にAWS Control Towerの設定がおわって、各アカウントのS3バケットのログをLogArchiveアカウントに集約させようとしたところ、以下のエラーに遭遇しました

│ Error: creating S3 Bucket (tfstate-bucket) Logging: operation error S3: PutBucketLogging, https response error StatusCode: 400, RequestID: E6C0, HostID: FPrRDGkLEU=, api error InvalidTargetBucketForLogging: The owner for the bucket to be logged and the target bucket must be the same.

これは、S3アクセスログの直接集約は、ロギング元バケットと送信先バケットのオーナーが同一である必要がある制約があるため発生しているようです
しかし、LogArchive用アカウントにS3バケットのアクセスログを集約できなければ、アカウントを作った意味がありません。

そこで、この問題を解決するのがS3レプリケーションです

S3レプリケーションとは
S3レプリケーションとは、S3バケットに新しいオブジェクトがアップロードされたら、自動的にレプリケーション先のバケットにコピーが生成される仕組みです。
同じリージョン内でも、他のリージョンでも、他のアカウントにもレプリケーション先のバケットに指定できます。
ほとんどのオブジェクトは15分以内にレプリケートされますが、場合によっては、数時間かかることもあります。RTCを活用すれば99.99%のオブジェクトを15分以内にレプリケートすることができます。

https://ramble.impl.co.jp/6255/
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html#setting-repl-config-crossacct
https://blog.serverworks.co.jp/how-to-configure-s3-bucket-replication-in-a-multiaccount-environment

上記を参考に設定を行います
LogArchiveアカウント側には以下を参考に、必要なIAMロールなどを作成します

https://qiita.com/sugimount-a/items/415e0672832868611829

https://dev.classmethod.jp/articles/summarize-principal-settings-in-s3-bucket-policy/