目の前に僕らの道がある

勉強会とか、技術的にはまったことのメモ

ECSのタグ付け認可とアカウント単位のオプトアウトの廃止

ECSのタグ付け認可とは

ECS関連のリソース作成時にリソースタグを付けることができます。その際 ecs:tagResource の権限が必要となります。なお、リソースタグを設定しないECSリソース作成の際は権限不要です。

この権限の有無のチェックをタグ付け認可と言います。

具体的にECSリソースの作成のアクションは以下の通りです。

  • CreateCapacityProvider
  • CreateCluster
  • CreateService
  • CreateTaskSet
  • RegisterContainerInstance
  • RegisterTaskDefinition
  • RunTask
  • StartTask

タグ付け認可の仕組みは2023年4月18日に導入されました。しかしながら従来からECSリソースを作成する際にタグ付けしていたAWSアカウントに関しては影響があるため、アカウントレベルでタグ付け認可の機能を無効(オプトアウト)することができました。つまりアカウントレベルで無効にしていれば ecs:tagResource の権限がなくてもタグ付けをすることが可能でした。

しかしながらアカウント単位のオプトアウト設定は2024年3月9日に廃止されます。

アカウント単位のオプトアウトの廃止

タグ付け認可におけるタイムラインは以下のとおりです

  • 2023年4月18日 タグ付け認可の導入とアカウント単位での有効化設定の導入
  • 2024年2月9日- 2月28日 新規アカウントおよび影響を受けないアカウントに関してデフォルトでタグ付け認可の有効化が行われる
  • 2024年2月29日 アカウント単位で有効にしている場合、無効に変更できなくなる
  • 2024年3月29日 すべてのアカウントでタグ付け認可が有効になり、アカウント単位での設定が不可能になる

現時点(2024/03/20)であまり時間がありません。

現在タグ付け認可に影響あるAWSアカウントに関しては、Personal Health Dashboadに以下のような通知が来ているはずです。

▼ElasticContainerService security notification (クリックで展開)▼

English follows Japanese | 英語のメッセージは日本語の後にございます

お客様のアカウントにて過去 1 年以内に ecs:TagResource の許可無しに ECS リソースの作成時にタグを付けていることが判明したため、ご連絡差し上げます。Amazon ECS は、2023 年 4 月 18 日にリソース作成のタグ付け認証を導入しました [1]。新規および既存のお客様は、ECS Console または API の ECS アカウント設定ページを使用して、この新機能の使用をオプトインする必要があります。このセキュリティ制御により、ECS リソースの作成時にタグをつけることをユーザーに拒否または許可できます。2024 年 3 月 29 日以降もお客様の IAM プリンシパルが新しく作成された ECS リソースに引き続きタグを適用できるように、IAM ポリシーを更新して ecs:TagResource アクションを明示的に許可することを強くお勧めします。


2024 年 2 月 9 日以降、AWS コンソール の ECS アカウント設定ページにて tagResourceAuthorization アカウント設定を明示的に off に設定していないすべてのお客様のアカウントは、自動的にこの設定にオプトインされました。お客様の AWS アカウントは一時的に許可リストに載せているため、2024 年 3 月 29 日まではタグリソース認証の off の動作が継続されます。

2024 年 3 月 8 日、現在オプトインしているアカウントが tagResourceAuthorization をオプトアウトする機能を削除し、タグをサポートするすべての ECS リソースの作成に際して ecs:TagResource IAM 権限の使用を強制するようにしました。

最終的に 2024 年 3 月 29 日をもってお客様のアカウントを許可リストから削除し、tagResourceAuthorization を有効化します。呼び出し元のプリンシパルの IAM ポリシーに ecs:TagResource アクションを含めずにタグをつけて ECS リソースを作成しようとすると、「AccessDenied」メッセージが表示されます。この変更は CreateCapacityProvider, CreateCluster, CreateService, CreateTaskSet, RegisterContainerInstance, RunTask, StartTask, および RegisterTaskDefinition の API に影響を及ぼします。

ecs:TagResource を使用しない拒否レスポンスの例

以下は、ecs:CreateCluster アクションを付与している IAM ポリシーの一部です。ecs:TagResource アクションは含まれていません。tagResourceAuthorization アカウント設定がオンの場合、リクエスト例では以下の AccessDenied 例外が返されます。

# IAM ポリシー

“Statement”: [
{
“Sid”: “AllowCreateCluster”,
“Effect”: “Allow”,
“Action”: [
“ecs:CreateCluster”
],
“Resource”: “*”
}
]


# クラスター作成のリクエスト

aws ecs create-cluster --cluster-name MyCluster --tags key=key1,value=value1

# タグ付けの拒否されたレスポンス

An error occurred (AccessDeniedException) when calling the CreateCluster operation:
User: is not authorized to perform: ecs:TagResource on resource: cluster/MyCluster because no identity-based policy allows the ecs:TagResource action

必要なアクション:

IAM プリンシパルが 2024 年 3 月 29 日以降も新しく作成された ECS リソースに引き続きタグを適用できるように、IAM ポリシーに次のステートメントを追加することを強くお勧めします。

すべての ECS リソースの作成時にタグ付けを許可
以下の説明に従って ecs:TagResource アクションを追加すると、ECS リソースの作成中にタグ付けが可能になります [2]。

“Statement”: [
{
“Sid”: “AllowTagging”,
“Effect”: “Allow”,
“Action”: [
“ecs:TagResource”
],
“Resource”: “*”
}
]

単一の ECS リソースタイプ (ECS クラスタ) の作成時にタグ付けを許可

条件ステートメント ecs:CreateAction を使用すると、タグ付けを特定の ECS API に制限できます。以下の例では、ECS CreateCluster API でのみタグ付けへのアクセスを許可します。タグ付きの ECS RunTask API へのリクエストは、拒否判定になります [2]。

“Statement”: [
{
“Sid”: “AllowClusterTagging”,
“Effect”: “Allow”,
“Action”: [
“ecs:TagResource”
],
“Resource”: “*”,
“Condition”: {
“StringEquals”: {
“ecs:CreateAction” : “CreateCluster”
}
}
}
]

タイムライン:

2024 年 2 月 9 日(完了)- タグ付け認証はデフォルトで on になっています。これには、ホワイトリストに登録されているアカウントは含まれません。tagResourceAuthorization アカウント設定の on/off を切り替えることも可能であり、ポリシーへの準拠をテストいただけます。

2024 年 3 月 8 日 - タグ付け認証を on にすると、off にすることはできなくなります。この日まではアカウント設定を切り替えることができますので、その間に IAM ポリシーをテストすることをお勧めします。

2024 年 3 月 29 日 - すべての AWS アカウントでタグ付け認証が有効になります。アカウントレベルの設定は使用されなくなり、AWS コンソールの ECS アカウント設定ページから削除されます。

ご質問やご不明点等ございましたら、AWS サポート [3] までお問い合わせください。

[1] https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#tag-resources-setting
[2] https://docs.aws.amazon.com/AmazonECS/latest/developerguide/supported-iam-actions-tagging.html
[3] https://aws.amazon.com/support

---

We are contacting you because we identified that your account has tagged ECS resources upon creation, within the past year, without the ecs:TagResource permission. Amazon ECS introduced tagging authorization for resource creation on April 18, 2023 [1]. New and existing customers must opt-in to use this new feature by using the ECS Account Settings page in the ECS Console or API. This security control allows users to deny or allow tagging ECS resources when they are created. We strongly recommend you update your IAM policies to explicitly allow the ecs:TagResource action so that your IAM principals continue applying tags to newly created ECS resources on or after March 29, 2024.

From February 9, 2024, all customer accounts which have not explicitly set the tagResourceAuthorization account setting to “off” in the ECS Account Settings page in the AWS Console were automatically opted into the setting. We have temporarily allow-listed your AWS account so you will continue to have the “off” behavior for tagResourceAuthorization until March 29, 2024.

On March 8, 2024, we removed the ability for currently opted-in accounts to opt-out of tagging authorization and enforced the creation of all ECS resources that support tags to use the ecs:TagResource IAM permission.

Finally on March 29, 2024, we will remove your account from the allow-list and activate tagResourceAuthorization. You will experience an "AccessDenied" message if you attempt to create tagged ECS resources without including the ecs:TagResource action in the IAM policy of the calling principal. This change will affect the following APIs: CreateCapacityProvider, CreateCluster, CreateService, CreateTaskSet, RegisterContainerInstance, RunTask, StartTask, and RegisterTaskDefinition.


Example Deny Response without ecs:TagResource

The following is part of an IAM policy that is granting the ecs:CreateCluster Action. It does not include the ecs:TagResource Action. When tagResourceAuthorization Account setting is on, the example request would return the AccessDeniedException below.


# IAM Policy
“Statement”: [
{
“Sid”: “AllowCreateCluster”,
“Effect”: “Allow”,
“Action”: [
“ecs:CreateCluster”
],
“Resource”: “*”
}
]

# Create Cluster Request
aws ecs create-cluster --cluster-name MyCluster --tags key=key1,value=value1

# Tagging Denied Response
An error occurred (AccessDeniedException) when calling the CreateCluster operation:
User: is not authorized to perform: ecs:TagResource on resource: cluster/MyCluster because no identity-based policy allows the ecs:TagResource action

Required Action:

To ensure your IAM principals continue applying tags to newly created ECS resources on or after March 29, 2024, we strongly recommend adding the following statement(s) to your IAM policies:

Allow Tagging during creation for all ECS Resources
Adding the ecs:TagResource Action as described below would Allow tagging during ECS resource creation [2].


“Statement”: [
{
“Sid”: “AllowTagging”,
“Effect”: “Allow”,
“Action”: [
“ecs:TagResource”
],
“Resource”: “*”
}
]

Allow Tagging during creation for single ECS Resource Type (ECS Cluster)

Using the Conditional statement ecs:CreateAction allow you to limit the tagging to a specific ECS API. The example below grants access to tagging only on the ECS create-cluster API. A request to the ECS API run-task with tags would result in a Deny decision [2].


“Statement”: [
{
“Sid”: “AllowClusterTagging”,
“Effect”: “Allow”,
“Action”: [
“ecs:TagResource”
],
“Resource”: “*”,
“Condition”: {
“StringEquals”: {
“ecs:CreateAction” : “CreateCluster”
}
}
}
]

Timeline:

February 9, 2024 (Completed) - Tagging Authorization is “on” by default. This excludes your account which is allowlisted. The tagResourceAuthorization account setting can be turned on/off to help test your policy compliance.

March 8, 2024 - Tagging Authorization can no longer be turned “off” once it is turned “on”. It is recommended that you test your IAM policies before this date while you are able to toggle the account setting.

March 29, 2024 - Tagging Authorization will be turned on for all AWS accounts. The account level setting will no longer be used and will be removed from the ECS Account Settings page in the AWS Console.

If you have any questions, please contact AWS Support [3].

[1] https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#tag-resources-setting
[2] https://docs.aws.amazon.com/AmazonECS/latest/developerguide/supported-iam-actions-tagging.html
[3] https://aws.amazon.com/support

通知が来ているアカウントは29日までに対応する必要があります。

確認

aws ecs list-account-settings --effective-settings --name tagResourceAuthorization を実行すると以下のような表示になると思います。ここがonであれば、すでにアカウント単位で有効になってるので影響がありません。(ただし、タグ付きのリソースを新規作成する際には権限が足りないとエラーになる可能性はあります)

ここがoffになっている場合、タグ付け認可が無効になってるので3月29日以降影響を受ける可能性があります。

% aws ecs list-account-settings --effective-settings --name tagResourceAuthorization
{
    "settings": [
        {
            "name": "tagResourceAuthorization",
            "value": "on",
            "principalArn": "arn:aws:iam::xxxxxxxxxxxx:root"
        }
    ]
}

影響がある例

ユースケースにもよりますが、タグ付け認可に関連する操作は以下のようなものが考えられるかと思います

  • インフラ担当者によるECSリソース構築
  • 開発担当者(またはCI/CD)によるECSサービスのデプロイ

前者に関しては、PowerUser相当の強い権限を付与されていることが多くここが問題になることはほとんどど無いかとは思います。

後者の特にCI/CDによるデプロイに問題となることがありえます。一般的に非人間ユーザで目的が明確であれば、最小権限の原則に則り、 ecs:TagResource が付与されていない可能性があります。

トライアンドエラーで権限を付与した場合、過去にうまく動いたためそのままの権限で使い続けている可能性もあります。その場合影響がある可能性あります。

デプロイ時のタスク定義登録の際、タスク定義内に従来なかったtagsの記述を新規追加した際にResgisterTaskDefinitionでエラーになるという事例を私は経験しました。 タスク定義にtagsがないときはタグ付け認可は実行されないのでそのまま成功していたため、ecs:TagResource が必要なことに気づいていませんでした。 エラーとしては以下のような記述になるので、タグ付け認可の機能の存在を知っていて冷静に読み解けば、ecs:TagResource が足りていないことに気づけると思います。

An error occurred (AccessDeniedException) when calling the RegisterTaskDefinition operation: User: arn:aws:sts::xxxx:assumed-role/deploy-github-actions/GitHubActions is not authorized to perform: ecs:TagResource on resource: arn:aws:ecs:ap-northeast-1:xxxx:task-definition/ecs-service because no identity-based policy allows the ecs:TagResource action

対応

まずECSサービスを利用しているIAM RoleとIAM Policyを洗い出します。

その上でそれらが以下のアクションを許可している場合、ecs:TagResource を追加してあげます。

  • CreateCapacityProvider
  • CreateCluster
  • CreateService
  • CreateTaskSet
  • RegisterContainerInstance
  • RegisterTaskDefinition
  • RunTask
  • StartTask

私の場合は、ECSサービスデプロイ用のポリシーに以下のStatementを追加しました。それぞれ適切な記述を足していただけたらと思います。

この場合タスク定義を登録する際にタグ付け認可を通すような許可を追加しています。

        {
            "Action": "ecs:TagResource",
            "Condition": {
                "StringEquals": {
                    "ecs:CreateAction": "RegisterTaskDefinition"
                }
            },
            "Effect": "Allow",
            "Resource": "arn:aws:ecs:ap-northeast-1:xxxxxx:task-definition/yyyyyyyyyyyyyyy:*",
            "Sid": "RegisterTaskDefinitionWithTag"
        },

まとめ

タグ付け認可について説明しました。タグ付け認可は2024年3月29日に強制的に全アカウントで有効になります。時間が少ないですが、影響受ける可能性があるかどうかチェックしてハマらないようにしましょう。

また、これまでタグ付けしてなかったリソースにタグ付けする際にタグ付け認可に引っかかる可能性があります。

デプロイやリソース作成の際にnot authorized to perform: ecs:TagResource と言われたらこの記事を思い出していただけたらと思います。

それでは良いECSライフを!

関連リソース

Terraformのtfstateについて考える

この記事は3-shake Advent Calendar 2023の23日目の記事となります。

こちらはSRE Tech Talk #6で話した内容に補足したものです。

資料はこちらとなります。

tfstateとは

Terraformが管理しているリソースの状態を表すjson形式のファイルです。

tfstateとterraformファイルと実際のリソースの状態を比較して、terraformコマンドが実行されます。

一般的には直接変更せずterraform stateコマンドを通して変更を行い、一般ユーザがtfstateに触れることはないです。

tfstateの課題

tfstateについて以下の課題があります。それぞれについて見ていきます。

  • tfstateの管理場所
  • tfstateを管理するリソースの管理
  • tfstateの分割

tfstateの管理場所をどうするか問題

主な保存場所候補としては以下のものがあります。

  • local(デフォルト)
  • クラウドのオブジェクトストレージ
  • Gitレポジトリ統合
    • GitLab
  • SaaS利用
    • Terraform Cloud

local

Terraformのデフォルト保存先です。Terraformを実行する同じディレクトリのterraform.tfstateに保存されます。

1人もしくは変更頻度が著しく低い状況など特殊なとき使えるものとなります。git管理して複数人で使うこともできるが、コンフリクトが発生しうるので、チーム開発には向かないです。

基本的には複数人でterraformを使用するときは非推奨です。

S3/Google Cloud Storage

監理するクラウドのオブジェクトストレージに保存する方法です。これが標準的(当社比)なのかなと思っています。

オブジェクトストレージなので、権限があればどこからでもアクセスすることができます。それゆえ、同時にTerraformが実行されるので排他ロックの処理が必要となります。S3バックエンドを使用した場合はDynamoDBを使用してstate lockを実現します。

Google Cloud Storageは単体でstate lockをサポートしています。

tfstateの参照権限をクラウドのIAMで制御する必要があります。

GitLab

GitLabでtfstateを監理することもできます。tfstateを管理するリソースを管理する必要がないことがメリットとなります。(後述します)

開発にGitLabを使っている場合、親和性が高い方法となります。

Terraform Cloud

GitLabと同様tfstateを管理するリソースを管理する必要がないというところにメリットがあります。

月間500 Managed Rsourcesまで無料で使えます。

web上からリソース差分の確認できたり、applyが可能です。SaaSクラウドのリソース情報を預けることに抵抗がない場合は選択肢としては有望です。

なおTerraformのStateのドキュメントではこういう記述があり、Terraform Cloudを推奨しているようです。

This state is stored by default in a local file named "terraform.tfstate", but we recommend storing it in Terraform Cloud to version, encrypt, and securely share it with your team.

昔はAWSと連携するためにIAM Userのアクセスキーを使わないといけなかったが、OIDC認証もできるようになったので、よりやりやすくなったかと思います。

tfstateを管理するリソースをどう管理する問題

GitLabやTerraform Cloudを使う場合には起きない問題となります。 S3のようなクラウドのオブジェクトストレージを使用する場合は、このS3バケットをどう作るかということが問題となります。コマンドで作る場合、コマンドの管理、terraformで作る場合はそのtfstateはどこに保存するか、そういったことに頭を悩ませます。そこについて考えていきます。

以下の方法が考えられます。

  • aws/gcloudコマンド
  • terraform + local state管理
  • CloudFormation

aws/gcloud コマンド

そもそも作成コマンドしか打たないのであれば、スクリプトをレポジトリに含めておけば良いという考え方はあります。 基本的に一度作れば変えることはないので、これで十分という風に割り切ることはできます。

ただし、tfstateのバケットだけでなく、CI/CD用のIAM RoleやOIDC認証リソースなども初期リソースとして含めて管理したいというユースケースだと、スクリプト管理では力不足になりうります。

terraform + local state 管理

オブジェクトストレージをterraformで作る方法です。ただし、tfstateに関してはlocalに保存し、これをgitも管理します。 かたくなにterraformを使いたい人に向けな方法となります。

デメリットとしては、tfstateもgit管理するのでコミット忘れがあります。また、頻度低いですがterraform自体はローカルで実行せざるを得ないので変更衝突が起きうることです。

CloudFormation / Google Deployment Manager

クラウドごとにコードを変えないといけない。IaCツールを2種類使うというそこはかとない気持ち悪さはあるというデメリットはありますが、gitでインフラ状態管理しなくてすむというメリットがあります。気持ち悪さだけを克服できるなら無難な選択肢だとは思います。

tfstateをどう分割するか問題

第一に考えるのが環境の分離。この分離の仕方だけ他とは系統が違うので独立して説明します。

一部差分があるだけで、以下のような形でほぼ同じ構成の環境を作ることはよくあります。

  • 開発環境
  • ステージング環境
  • 本番環境

これらについてどう分割するのかを考えていきます。

環境分離パターン

大きく2つのパターンを利用することが多いです。それぞれ見ていきます。

ディレクトリ分離パターン

これは環境ごとにディレクトリを分割して、環境ディレクトリを実行単位とします。環境の切り替えはディレクトリ移動することで行います。 環境ごとの差分が大きいときに使うことが多いです。デメリットとしては環境ごとにリソース定義をそれぞれ書くので記述量が多くなるというのがあります。そのため、可能な限りモジュール化して、なるべくパラメータだけの差分にするようにします。

ディレクトリ構成例としては以下の通りです。

.
├── envs
│   ├── dev
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── prd
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── stg
│       ├── locals.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── modules
    ├── vpc
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── application
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf

backend-configパターン

backend-configオプションとvars-fileオプションを組み合わせて、環境を切り替えるパターンです。

${ENVDIR}/terraform.tfvars に環境ごとの差分パラメータを定義して、${ENVDIR}/backend.tfvars に環境ごとのtfstate保存先を定義します。

terraform initbackend.tfvars を切り替えることで環境の切り替えを行います。

環境ごとに差分が少ないときに向いています。差分は terraform.tfvars に記述されているパラメータだけなので、記述量が少なくて済みます。 ただし差分が多くなるとcount, for_eachで分岐やループを作ることになり読みにくくなるというものがあります。

ディレクトリ構成例としては以下のようになります。

.
├── envs
│   ├── dev
│   │   ├── backend.tfvars
│   │   └── terraform.tfvars
│   ├── prd
│   │   ├── backend.tfvars
│   │   └── terraform.tfvars
│   └── stg
│       ├── backend.tfvars
│       └── terraform.tfvars
├── locals.tf
├── main.tf
├── modules
│   └── vpc
│       ├── locals.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── outputs.tf
├── provider.tf
└── variables.tf

設定ではbackendをs3と指定しておき中身はオプションで指定するようにします。

terraform {
  backend "s3" {}
}

以下のようにterraform initするたびに適用する環境を切り替えることができる。

terraform init --backend-config=${ENVDIR}/backend.tfvars --reconfigure
terraform apply --var-file=${ENVDIR}/terraform.tfvars

workspace

workspaceは同じような環境を複製するときに使ういます。シングルテナント環境を量産する場合や開発環境を複数作る場合などに使います。環境を切り替える用途には作られてないとドキュメントまでは記載されています。

In particular, organizations commonly want to create a strong separation between multiple deployments of the same infrastructure serving different development stages or different internal teams. In this case, the backend for each deployment often has different credentials and access controls. CLI workspaces within a working directory use the same backend, so they are not a suitable isolation mechanism for this scenario.

自分自身がworkspaceを実運用で使ったことがないので多くは語れないです。別でちゃんと使ってから書きたいと思います。

環境分離以外の分割をどうするか問題

小さいサービスでは環境を分離するだけでだいたいは問題ないことがおおいですが、terraformを運用していると運用面、管理面でいろいろ課題が出てくると思います。

管理するリソースが増えるとplan/applyの時間が増えたり、リソースの見通しが悪くなったりしてきます。

特に実行時間が意外に馬鹿にできなかったりします。下手するとplanに数分かかるようになったりします。

そのため、ある程度大きくなったらtrstateを分割して、リソースの管理範囲を分割する必要が出てきます。

これをどうやって分割するかが自分の中で答えが出ていない出てないし、分脈によって解決策は異なるとは思います。

ここで、解決策を考えるうえで、分割するための観点を見ていきましょう。

分割する観点

分割する観点は以下のようなものがあるかと思います。

  • プロバイダー
  • 管理権限
  • 変更頻度

プロバイダーで分割

プロバイダー単位で分割するパターンです。

例としてはAWSとDatadogのようにプロバイダーで分割します。プロバイダー間で依存がない場合は分けやすいかと思います。

また、プロバイダー間で管理主体が違うことも多いので素直な分け方だとは思います。

しかしながら、アプリケーションリソースとアプリケーションの監視を近いところにおいたほうが見通しがよいのではという観点もあるので運用体制にあわせて考えるとよいでしょう。

管理権限で分割

チームの権限で分割するパターンです。ただし、より堅くするなら、ディレクトリではなくレポジトリ自体も分割して、コードの参照権限も分割する方が望ましい場合もあります。

    • ネットワーク ⇒ インフラチーム
    • アプリケーション ⇒ 開発チーム

変更頻度で分割

変更をあまりしないリソースを変更が頻繁なリソースと一緒のplan/applyするのは無駄なので変更の頻度でtfstateを分割するパターンもあります。

    • 変更が少ない ⇒ DB/ネットワーク
    • 変更が多い ⇒ EC2/ECS

依存の方向性で分割

少し観点を変えてみます。実際に分割をした場合に問題となるのはtfstate間のリソースの依存が課題になります。 tfstate間で相互に依存するようなコードを書くとtarget指定してそれぞれのstateのリソースを作成しなくてはなりません。 こうすると管理が煩雑となってしまうので、原則的に片方向だけの依存になるように分割するようにするのが望ましいです。

tfstate間のリソース参照

terraform_remote_state を使うことで、参照元のTerraformでoutputした内容を別のTerraformで利用することができます。

# 参照元 networkアカウント
output "vpc_id" {
  value = aws_vpc.main.id
}
# 参照先 applicationアカウント
# data.terraform_remote_state.network.vpc_id の形式でVPC IDを参照できる
data "terraform_remote_state" "network" {
  backend = "s3"

  config {
    bucket = "terraform-tfstate-network-xxxxx"
    key    = "tfstate"
    region = "ap-northeast-1"
  }
}

まとめ

正直tfstateをどう扱うかに正解はないです。サービス規模や性質によって選択は変わります。

本当に小さい規模であれば、tfstateを分割せず一つで十分でしょうし、チーム開発せず一人で扱うなら、通常であれば推奨されないtfstateのlocal git管理という手段がふさわしい場合もあります。 また、組織やサービスの成長や時間経過によっても最適な選択は変わると思います。大事なのは選んだ技術要素に関しては選定理由を説明できるようにはしておくということです。 選定理由及び不採用理由を明確にしておくことで、変更時に最適な選択の助けになるでしょう。

Terraform使いがPulumiに入門しました

この記事は3-shake Advent Calendar 2023の16日目の記事です。

qiita.com

この内容はSRETT #8で発表した内容に補足しています。

3-shake.connpass.com

前提

筆者は以下の背景を持っています。

  • 普段はAWSをメインに触っている
  • 普段はTerraformをメインで使ってる
  • Pulumiはプロダクションでは使ったことがない
    • ちゃんとは把握できてない

語らないこと

以下のようなPulumi以外の基本的なことは語りません

  • IaCとは
    • 概要、特徴、メリット・デメリット
  • Terraformとは
    • 概要、特徴、メリット・デメリット、操作方法

モチベーション

なんでPulumiを今回調べようかと思った動機について書こうと思います。

Terraformの記述力に限界を感じていたというところが大きいです。以下の点がつらいかなと思っていたところです。

  • 足りない関数
  • 二重ループのためのModule使用
  • 分岐処理のためのcountと三項演算子

とはいえ、記述力が低いからこそ複雑なことを抑制できて可読性が上がっている面もあると思います。 冗長でも、可読性が高いというのはメリットではあります。

他の選択肢としては以下のものがあるかと思います。

  • CDK
    • AWSに限定される
  • CDKTF(CDK for Terraform)
    • 結局terraformのJSONコードに変換されるので、terraformに依存します
    • それ自体は悪くないが、どうせならTerraformから離れたものを学びたい

そこでなにか良いものがないかと思い当たったところにPulumiがあったので調べてみようとなりました。

Pulumiとは

Pulumiはプログラミング言語でインフラを構築可能なプロビジョニングツールです。

Terraformと同じようにProviderを通して複数のクラウドに対応しています。

TerraformはHCLという宣言的言語を使用するのに対し、Pulumiは汎用的なプログラミング言語を使用してインフラリソースを定義します。

Pulumi - Infrastructure as Code in Any Programming Language

対応言語

参考: Pulumi Languages & SDKs | Pulumi Docs

Pulumiのアーキテクチャ

以下のようの構成になっています。

参考: How Pulumi Works | Pulumi Docs

  • Language host
    • インフラリソースの定義を Program (後述)として好きな言語で定義します。
  • Deployment Engine
    • 希望する状態に変更するための操作セットを実行する役割を果たします。
  • Resource Provider
    • クラウドサービスとの通信を処理して、Programで定義したリソースの変更処理を行います。

上記の例だと、Programにリソースの定義がある場合、Stateと比較して、管理されているリソースであるかを確認します。

存在すれば、プロバイダーを通して実際のクラウドのリソースの状態と比較して差分があれば適用。

存在しない場合、プロバイダーを通してリソースを作成。

Pulumiのコンポーネント

What is Pulumi? | Pulumi Docs

Pulumiのコンポーネントは以下のようになっています。

  • Project
  • Program
    • インフラのあるべき姿を定義したもの
  • Resource
    • インフラを構成するオブジェクト。ResourceのプロバティはOutputとして他のResourceのInputに使用することができます
  • Stack
    • Programを実行すると作成されるインスタンス。同一のProgramから開発、ステージング、本番環境のStackを個別に作成することができます。

Pulumi Cloud

Terraform Cloudのようなものと考えていただいて良いです。 デプロイの状態、履歴やシークレットを管理して、CI/CDやGitHubと連携してデプロイを実行することもできます。 Pulumi CLIはバックエンドを明示的に指定しない限りはでデフォルトでPulumi Cloudを使用します。

Terraformはデフォルトでlocalバックエンドを使用します。

以下はPulumi Cloudの画面です。

Pulumi Cloud 料金

個人で使う限りは無料で使用することができます。

※2023/12/18現在

Pulumi操作方法

ここからPulumiの操作方法を見て行きたいと思います

Pulumiインストール

個人的にはバージョン管理したいのでasdfでインストールします。brewでもインストールできます。

# .tool-versions
pulumi 3.97.0 
asdf install

Pulumi Cloudへログイン

デフォルトではPulumi Cloudへログインします。以下のコマンドを実行するとブラウザが起動するので、ログイン処理をします。

pulumi login

Pulumi Cloudを使わず、ローカルにstateを保存したい場合は以下のとおりです。

pulumi logout
pulumi loign --local

Projectの作成

pulumi new コマンドで新しいProjectを作成できます。同時にStackも作成されます。引数にテンプレートを指定できます。 ウィザード形式で設定をすることができます。

以下の例は awsプロバイダーを使用して、言語はTypeScriptを使用するテンプレートとなります。

ディレクトリ内にはPulumi実行に必要な各種ファイルが生成されます。 ここで見るべきは以下の3ファイルです。

  • Pulumi.yaml
    • プロジェクト設定
  • Pulumi.dev.yaml
    • Stack(dev)設定
  • index.ts
    • リソース定義
# Pulumi.yaml
name: sample
runtime: nodejs
description: A minimal AWS TypeScript Pulumi program
# Pulumi.dev.yaml
config:
aws:region: us-east-1
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

// Create an AWS resource (S3 Bucket)
const bucket = new aws.s3.Bucket("my-bucket");

// Export the name of the bucket
export const bucketName = bucket.id;

変更を確認

plumi preview コマンドでStackの変更差分を確認できます。 terraform plan を似ていますが、こちらは差分の詳細は表示されません。

Stackデプロイ

pulumi up コマンドでStackをデプロイできます。 terraform planterraform apply を組み合わせた挙動になります。

実行すると選択肢が出ます。

details を選択すると変更差分の詳細が表示されます。

yesを選択すると、変更が適用されます。

リソース削除

pulumi destroy でStackを削除できます。 pulumi up と同じようにdetailsで詳細表示、 yes で削除実行ができます

state操作

PulumiではStackごとにStateが保存されています。 Stateを操作するコマンドは以下のとおりです。

  • state出力(terraform state pull 相当 )
    • pulumi stack export
  • state インポート(terraform import相当)
    • pululmi import <TYPE> <NAME> <ID>
  • state 削除(terraform state rm 相当)
    • pulumi state delete <URN>

Terraformからの移行

Terraformからの移行オプションは以下の通りとなります。

  1. terraformとPulumiを共存する
    • Pulumiからtfstateを参照する
  2. tfstateからリソースをPulumiへインポートする
  3. TerraformのコードをPulumiのコードに変換する

参考: Adopting Pulumi | Pulumi Docs

参考: Migrating from Terraform | Pulumi Docs

TerraformとPulumiを共存する(tfstateを参照)

networkリソースに関しては既存のterraformを使いつつ、そのoutputをPulumiで使うイメージになります。 以下のようなコードでlocalのtfstateが参照できるので、値を参照して利用することができます。

import * as aws from "@pulumi/aws";
import * as terraform from "@pulumi/terraform";

// Reference the Terraform state file:
const networkState = new terraform.state.RemoteStateReference("network", {
    backendType: "local",
    path: "/path/to/terraform.tfstate",
});

// Read the VPC and subnet IDs into variables:
const vpcId = networkState.getOutput("vpc_id");
const publicSubnetIds = networkState.getOutput("public_subnet_ids");

// Now spin up servers in the first two subnets:
for (let i = 0; i < 2; i++) {
    new aws.ec2.Instance(`instance-${i}`, {
        ami: "ami-7172b611",
        instanceType: "t2.medium",
        subnetId: publicSubnetIds[i],
    });
}

tfstateからインポート

pulumi import --from terraform ./terraform.tfstate のようにすることによってtfstateからリソースをインポートすることができます。

terraformからコード変換

pulumi convert --from terraform コマンドを使用することで、既存のTerraformのコードをPulumiのコードに変換することができます。

ただし、変換できないコードはTODOコメントが付く。90%~95%は変換が対応しているとのこと。

pulumi convert --from terraform --language typescript

まとめ

Pulumiの概要と基本操作をTerraformと対比しながら説明してきました。

  • 新規プロジェクトである程度複雑な処理をしたい。
  • プログラミング言語に精通している人がメンバーにいる。

そういった場合にはPulumiは良さそうに思えます。

しかしながら、ある程度Terraformで出来上がっているプロジェクトをPulumiに移行するのはそれなりに大変なので、プロジェクトの規模感とコストに見合うかを考えて導入するか考えると良いでしょう。

また、複雑なことをしたいというのは、本当に必要とされていることなのでしょうか?冗長でも簡易的な書き方をした方が望ましい場合もあるかと思います。そのあたりの目利きをちゃんと考えたいところです。

自分自身まだまだ使いこなせていないですし、追いきれてないPulumiのトピックもあるので、今後も選択肢の一つとして調べていきたいところです。

Terraformのsopsプロバイダーを使用するだけで機密情報は守られるのか

qiita.com

この記事は、3-shake Advent Calendar 2023の9日目の記事となります。

sops プロバイダーとは

sopsプロバイダーはMozilla sopsを使用して暗号化されたファイルから機密情報を取り出して、terraform上で使用できるようにしたものです。 暗号化の鍵をAWS KMS等を使うことにより、KMSキーを使う権限を持つ人だけ機密情報にアクセスできるようにするものです。 sopsで機密情報を暗号化することにより、平文で機密情報をgitレポジトリに保存することがなくなり安全ということになります。

機密情報を管理したい。でも平文では保存したくない。そういう用途にこちらは使用されます。

本当に安心?

SOPSを使って機密情報を暗号化することによりgitレポジトリには機密情報が平文で残らない。

これで安心と言われていますが、よく考えると機密情報をterraform実行時にはリソースに対して平文で与えているはずです。 つまり、tfstate上は機密情報が平文で保存されています。

例えば、tfstateがS3に保存されているとして、KMSキーへの権限がない人でもS3バケットにアクセスする権限があれば、平文の機密情報が見れてしまいます。 あまりないと思いますが、tfstateをlocalに保存するようにしていてそれをgit管理していてらなんのために暗号化しているのか。。。。ということになります。

こう考えると組織のポリシーによるが、sopsプロバイダーによる暗号化では不十分ではないかという疑問が生まれます。

ドキュメントを調べる

まずプロバイダードキュメントを当たってみます。

Docs overview | carlpett/sops | Terraform | Terraform Registry

To prevent plaintext secrets from being written to disk, you must use a secure remote state backend. See the official docs on Sensitive Data in State for more information.

これが意味してるのはバックエンドをlocalにした場合平文で機密情報が書かれるので、安全なリモートバックエンドを利用すべきということだと思います。

State: Sensitive Data | Terraform | HashiCorp Developer

参照しろと言われたドキュメントの該当部分を読んでみましょう。

ローカルディスクにtfstateを保存した場合は、機密情報が平文で保存されます。リモートにtfstateを保存する場合、保存時に暗号化されるかはバックエンドに依存します。

基本的にリモートステートを使うことを推奨しています。 例えば、Terraform Cloudを使う場合、tfstateは暗号化され、転送時もTLSで暗号化されます。 S3を使う場合もSSE-S3やSSE-KMS等でサーバサイド暗号化を有効にしておくことで、保管時の暗号化がされます。バケットポリシーでHTTPSを強制することで通信時の暗号化も保証することができます。

参考: 暗号化によるデータの保護 - Amazon Simple Storage Service

参考: Amazon S3 のセキュリティのベストプラクティス - Amazon Simple Storage Service

ところがですね。保存時、通信時の暗号化をしても、terraform state pullすると平文でtfstateが手に入ってしまうんですよ。。。後述します。

挙動を実験する

以下のような設定ファイルを作ります。sopsで暗号化したdb_userとdb_passwordをパラメータストアに設定するものになります。

tools-versions

terraform 1.5.5
sops 3.7.3

main.tf

terraform {
  required_version = "~> 1.5.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.15"
    }
    sops = {
      source  = "carlpett/sops"
      version = "~> 0.7.2"
    }
  }
  backend "s3" {
    region  = "ap-northeast-1"
    bucket  = "xxxxxxxxxx"
    key     = "test.tfstate"
  }
}

provider "sops" {
}

provider "aws" {
  region = "ap-northeast-1"
}

data "sops_file" "secrets" {
  source_file = "secrets.yaml"
}

resource "aws_ssm_parameter" "db_user" {
  type     = "String"
  name     = "/test/db_user"
  value    = data.sops_file.secrets.data.db_user
}

resource "aws_ssm_parameter" "db_password" {
  type     = "SecureString"
  name     = "/test/db_password"
  value    = data.sops_file.secrets.data.db_password
}

暗号化前の secrets.yaml

db_user: user
db_password: password

apply結果がこちらとなります。

terraform apply

% export SOPS_KMS_ARN=arn:aws:kms:ap-northeast-1:xxxxxxxxx:key/yyyyyyyyyyyyyyyyyy
% terraform apply
data.sops_file.secrets: Reading...
data.sops_file.secrets: Read complete after 1s [id=-]

Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ssm_parameter.db_password will be created
  + resource "aws_ssm_parameter" "db_password" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/test/db_password"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "SecureString"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

  # aws_ssm_parameter.db_user will be created
  + resource "aws_ssm_parameter" "db_user" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/test/db_user"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "String"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_ssm_parameter.db_password: Creating...
aws_ssm_parameter.db_user: Creating...
aws_ssm_parameter.db_user: Creation complete after 0s [id=/test/db_user]
aws_ssm_parameter.db_password: Creation complete after 0s [id=/test/db_password]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
terraform apply  8.91s user 0.78s system 124% cpu 7.811 total

state showするとパラメータストアなのでsensitive扱いになっていて、見れません。これはいけるか?

terraform state show

% terraform state show aws_ssm_parameter.db_password
# aws_ssm_parameter.db_password:
resource "aws_ssm_parameter" "db_password" {
    arn       = "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_password"
    data_type = "text"
    id        = "/test/db_password"
    key_id    = "alias/aws/ssm"
    name      = "/test/db_password"
    tags_all  = {}
    tier      = "Standard"
    type      = "SecureString"
    value     = (sensitive value)
    version   = 1
}

% terraform state show aws_ssm_parameter.db_user    
# aws_ssm_parameter.db_user:
resource "aws_ssm_parameter" "db_user" {
    arn       = "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_user"
    data_type = "text"
    id        = "/test/db_user"
    name      = "/test/db_user"
    tags_all  = {}
    tier      = "Standard"
    type      = "String"
    value     = (sensitive value)
    version   = 1
}

ここで、terraform state pullをしてみて、tfstateファイルをローカルにダウンロードします。

そのtfstateファイルの中の該当部分はこちらとなります。

    {
      "mode": "managed",
      "type": "aws_ssm_parameter",
      "name": "db_password",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_password",
            "data_type": "text",
            "description": "",
            "id": "/test/db_password",
            "insecure_value": null,
            "key_id": "alias/aws/ssm",
            "name": "/test/db_password",
            "overwrite": null,
            "tags": null,
            "tags_all": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "password",
            "version": 1
          },
          "sensitive_attributes": [
            [
              {
                "type": "get_attr",
                "value": "value"
              }
            ]
          ],
          "private": "bnVsbA==",
          "dependencies": [
            "data.sops_file.secrets"
          ]
        }
      ]
    },

tfstateファイルの中身をよく確認するとしっかり平文で見えています。残念。

"value": "password",

結論

sopsプロバイダーを使用することによりgitレポジトリ上に機密情報を平文で保存することはなくなります。 しかしながら、tfstateのデータ上では設定値が平文で保存されることを防ぐことはできません。terraform state pullする権限があれば、機密情報が見れてしまいます。

運用組織のポリシーで、tfstateへのアクセス権限を適切に権限管理することができるのであれば、選択肢としては取りうります。 暗号化のためのKMSキー、tfstateを保存するS3バケットを機密情報をアクセス可能な人のみ権限を与えることが徹底できればよいです。 しかしながら、機密情報をいかなる場合でもローカルに平文で保存することが許容されない組織であれば、機密情報は手動で設定することを選択したほうが望ましいと思います。

どうしても機密情報をterraformで管理したのであれば、クライアントサイドで暗号化した機密情報をterraformで管理し、アプリ等で使用時にクライアントサイドで復号を行う形も考えられます。

安全かどうかは、tfstateの保存場所、tfstateへのアクセス権限、暗号化鍵のアクセス権限それぞれが適切に設定されているかどうかが鍵となります。

他に何かうまい方法で機密情報を管理しているという方がいらっしゃれば、ご意見ください。

ワークアラウンド

これは自分がよく使う手段となります。

リソースの箱だけ作って、作成時にダミーの値を入れておき、実際の値は手動で設定するという手法です。 ignore_changesを入れておくことで、手動で値を変更しても、terraform的には差分ができないようにしています。 これにより、機密情報をterraformの外に追い出しつつも、機密情報を入れるリソース自体は監理するということが実現できます。

resource "aws_ssm_parameter" "db_password" {
  type     = "SecureString"
  name     = "/test/db_password"
  value    =  "Dummy"
  lifecycle {
    ignore_changes = [value]
  }
}

吉祥寺.pm35 でLTしてきました。 #kichijojipm

吉祥寺.pm こと 句会

吉祥寺.pm35 に参加して、LTしてきました。

kichijojipm.connpass.com

資料はこちら。

言いたいこととしてはベストプラクティスなんてないよ。一般的によりよいプラクティスやパターンはあるけど、どんなときには適用できる銀の弾丸的なものはないから、自身の組織とサービスに合わせてくみ上げていきましょうということ。

正解はひとつ!じゃない!!

その上で、ざっくりとどんな選択肢と選択するための観点を述べていきました。まだ全然ブラッシュアップできるのでどこかでまとめてブログに書きたいところです。

ちなみに最後に出てくる あなたらしく○○ は同僚のスライドのパロディです。

毎回時間オーバーするのでトークで申し込んだ方が良いのでは?というツッコミはごもっともです。

懇親会でもTerraformのお悩みとか短いですが話せて楽しかったです。また参加したいですね。