🏄

AWS:CloudShell+CDK+GitHub で簡単 IaC

2024/12/30に公開

はじめに

フルタイム労働しながら個人開発をしていると、個人開発で自分がやったことというのは、まぁ忘れます。というわけで最近はすべての設定をコードとして残す方向に舵を切っています。

AWS では CDK を使うことですべてのリソースをコードとして管理できます。クライアント側には CloudShell を使用するのが手間的にもセキュリティ的にも現状最適解だと思いました。

本記事では GitHub でソースコードを管理し、CloudShell + CDK で AWS にリソースをデプロイする方法を紹介します。今回紹介する構成は

  • 数時間でレクチャー可能
  • 手順は最小限
  • 覚えることも最小限
  • AWS アクセスキーに起因するセキュリティリスクがない

といった利点があり、小規模な開発チームにおいては何も考えずに選択できるくらいに、初動として十分な効果を持った一手になると思います。ご興味があればぜひご覧ください。

AWS CDK とは

AWS CDK(Cloud Development Kit)は AWS の IaC(Infrastructure as Code)ツールです。CDK とか IaC についてはこの記事で詳解しませんので、公式ドキュメントや他の記事を参考にしてください。

IaC とは要するにインフラをコードによって宣言的に管理するための仕組みです。

プログラミングの世界には「手続的」と「宣言的」という2種類のアプローチがあります。

「手続的」とは、データやリソースに対して決まった手順で操作を行うことで目的を達成するアプローチです。スクリプトやプログラムで比較的容易に仕組みを作れる反面、行う操作の挙動は対象の状態に依存して変わってしまうため、管理には向いていません。たとえば「ユーザーを作成する」という操作は既にそのユーザーが存在する場合に再度実行するとエラーになりますので、何度実行しても同じ結果になってほしければ[1]、以下のようなスクリプトを書く必要があります。

  1. ユーザーが存在しなければ、作成する。
  2. ユーザーが存在して、設定が正しければ、何もしない。
  3. ユーザーが存在して、設定が正しくなければ、ユーザーを削除して再度正しい設定で作成する。

これだけでも条件分岐が最低2回必要になるかなり面倒な処理であり、すべてのリソースに対して同様の配慮をしていたら発狂してしまいます[2]。そこで注目されているのが宣言的なアプローチです。

「宣言的」とは、データやリソースが取るべき状態を定義することで、エージェントに目的を達成させるアプローチです。データやリソースがこうあって欲しいという理想の状態を書くだけで、その状態を達成するために必要な操作はエージェントが判断して実行してくれるため、管理に向いています。たとえば「このような権限を持ったユーザーを作成してほしい」と宣言しておけば、存在しない場合には作成、権限が異なる場合は修正といった対応をエージェントが実行します。欠点は複雑なエージェントを実装しなければならないことであり、普通はサービスやツールとして提供されているものをそのまま使用します。

AWS CDK は AWS CLI をベースとした IaC 用のクライアントであり、AWS CloudFormation をバックエンドのエージェントとして使用します

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

https://aws.amazon.com/jp/what-is/iac/

https://aws.amazon.com/jp/cli/

https://aws.amazon.com/jp/cloudformation/

AWS CloudShell とは

AWS CloudShell は AWS のマネジメントコンソールから起動できるシェル環境です。

どのように実装されているのかはいまいちよく分かっていませんが、リージョン単位、VPC単位、IAMユーザー単位など一定のルールで隔離されたシェル環境が生成され、そこにアクセスできるようです。おそらく特殊なプライベートネットワーク内に設置されたコンテナだと思われます。

AWS CloudShell には AWS CLI などの管理ツールがプリインストールされており、マネジメントコンソールにログインしているユーザーが持っている権限でリソースに対する操作が可能です。また、インターネットへのアクセスが可能であり、ホームディレクトリは永続化されていて1GBまで使用可能なので、簡単なツールであれば自分でインストールすることも可能です[3]

CloudShell の利点は、面倒な AWS CLI の接続設定をしなくてよいことだと思います。AWS CLI をローカル環境に構築すると、アクセスキーを発行した上で流出しないよう管理に気を使うことになりますし、会社のプロキシ下で利用する場合にはプロキシを突破するための設定をしなければなりません。

CloudShell はその辺りの設定をしなくてもデフォルトでマネジメントコンソールにログインしているユーザーの認証情報で AWS CLI を使用できますし、アクセスキーが流出する心配がないのでセキュリティ的にもローカルから使用するより安全です重要なリソースの管理は CloudShell から行うほうがよいでしょう

https://docs.aws.amazon.com/ja_jp/cloudshell/latest/userguide/welcome.html

https://blog.usize-tech.com/aws-cloudshell-deepdive/

CloudShell から CDK を使う

さて、CloudShell と CDK を使うメリットについては解説しましたので、早速 CloudShell 上で CDK を使えるようにしましょう。CloudShell を使用する IAM ユーザーには適当な権限を与えておいてください。分からない場合はとりあえず AdministratorAccess を与えておけば間違いないです。

CloudShell 上で CDK を使用する際に気を付けるべきポイントは、CloudShell で普通に npm install を実行するとなぜかフリーズするという点だけです[4]

この問題を解消するためには、nvm を用いて CloudShell のホームディレクトリに node を仮想化する必要があります[5]

CloudShell に nvm をインストールする方法は、nvm の公式リポジトリにしたがってください[6]

https://github.com/nvm-sh/nvm

私がインストールしたときは curl でスクリプトを落として bash で実行するだけでした。その後、node をインストールします。node の最新版は不安定[7]なので、LTS 版を使用します。

# nvm のインストール
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# nvm のアクティベート(CloudShell を再起動するなら不要)
source ~/.bashrc

# node の LTS 版をインストール
nvm install --lts

これで node がホームディレクトリに仮想化されました。続いて CDK をグローバルインストールします。CDK はプロジェクトの初期化時にディレクトリが空であることを要求するので、グローバルにインストールすることを推奨されています[8]nvm で仮想化されているとはいえ、CDK をグローバルインストールしなければならないことも、ローカルPCではなく CloudShell を使用するモチベーションのひとつです。

npm install -g aws-cdk

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html

以下のコマンドを実行し、cdk コマンドの使用方法が表示されればインストールは完了です。

cdk --help

最後に、AWS 側にも CDK からの操作を受け入れる設定をする必要があります。CloudShell から以下のコマンドを実行してください。

cdk bootstrap

上記の実行が完了すれば、CDK からリソースを操作するための準備は完了です。

CloudShell + CDK + GitHub で簡単管理

CloudShell は外部ネットワークへのアウトバウンド通信は許可しているので、GitHub からコードをダウンロードできます。今回はプライベートリポジトリに Deploy Keys を設定する方法を紹介します。Deploy Keys はリポジトリ単位で限定的なアクセス権限を与えるための簡易的な仕組みであり、環境に GitHub アカウントへのフルアクセスを許可したくない場合[9]に役立ちます。

https://docs.github.com/ja/authentication/connecting-to-github-with-ssh/managing-deploy-keys

リポジトリは適当に用意してください。今回はリポジトリ名を仮に my-repository として新たに作成したものとします。

まずは CloudShell から以下のコマンドを実行して、SSH キーを生成します。パスフレーズは設定したほうがセキュアですが、CloudShell を使えるということはマネジメントコンソールにアクセスできているということなので、その時点でセキュリティは終わっています。面倒ならば設定しなくともよいでしょう。

ssh-keygen -t ed25519

生成したキーペアの公開鍵を以下のコマンドで表示し、添付画像のようにリポジトリの Deploy Keys に設定してください。権限は Read だけでもよいですし、CloudShell で行ったソースコードの変更を GitHub にアップロードする可能性があるなら Read/Write を与えておきましょう。

cat ~/.ssh/id_ed25519.pub

さらに、CloudShell の ~/.ssh/config に以下のような内容を書き込んでください。Host の部分はあとで git コマンドから使います。好きなように設定してください。

~/.ssh/config
Host github.com-my-repository
    Hostname github.com
    IdentityFile=/home/cloudshell-user/.ssh/id_ed25519

ここまでの設定が完了していれば、CloudShell から以下のようにリポジトリを clone することが可能です。@ の後ろには ~/.ssh/configHost に書いたホスト名を指定してください[10]。また、username の部分はリポジトリが置かれているアカウント名で書き換えてください。

git clone git@github.com-my-repository:username/my-repository.git

ここまでできたら、あとはリポジトリに行った変更を git pull して cdk deploy するだけで、定義したリソースをデプロイできます。

コーディング

ここまでの手順で CloudShell には GitHub を介してソースコードを送り込めるようになっているので、以降の開発はローカルPCでやっても問題ありません。CloudShell でソースコードを編集したい場合は、GitHub にアップロードするために Deploy Keys に Write 権限を与えておいてください。

my-repository に移動して、CDK 用のディレクトリを切って初期化します。cdk init コマンドは初期化するディレクトリが空であることを要求するため、README や .git が置かれるリポジトリのトップディレクトリは初期化するのに適していません。今回は base というディレクトリを切ることにします。

# cdk init のためにサブディレクトリを切る
cd my-repository
mkdir base

# ディレクトリを初期化する
cd base
cdk init --language typescript

使用する言語はなんでもよいと思いますが、Python や Go など他の言語を使う場合でも AWS CLI を経由するために結局は node を使用するので、ホームディレクトリの1GB制限や追加で必要となる環境構築の手間を考えると TypeScript で書いておくのが無難だろうと思います。

初期化が完了するとリポジトリは以下のようなディレクトリ構成になります。

リポジトリのディレクトリ構成
my-repository
├── README.md
└── base
    ├── README.md
    ├── bin
    │   └── base.ts
    ├── cdk.json
    ├── jest.config.js
    ├── lib
    │   └── base-stack.ts
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── test
    └── tsconfig.json

重要なのは bin/base.tslib/base-stack.ts です。最初は以下のような内容が書かれていますので、他の記事を参考にするなり ChatGPT に聞くなりして、お好きな設定を書き込んでください。base/.gitignore に無視すべきファイルがしっかり書かれているので、リポジトリに追加するときは git add base のように雑な追加をしても問題ありません。

base/bin/base.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { BaseStack } from '../lib/base-stack';

const app = new cdk.App();
new BaseStack(app, 'BaseStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
base/lib/base-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class BaseStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'BaseQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

書き込んだら以下のようなコマンドを CloudShell の my-repository/base で実行してみてください。デプロイするリソースによっては料金が発生するのでご注意ください[11]。CDK を実行する IAM ユーザーには Cost Explorer へのアクセス権を与えておく[12]とよいでしょう。

# 定義した設定と現在の設定の差分を確認する
cdk diff

# 定義した stack すべてをデプロイする
cdk deploy --all

デプロイされたスタックは CloudFormation から確認できます。間違えてデプロイしてしまったスタックを消す場合など、CDK から操作するのが面倒なときはこちらから直接操作するとよいでしょう。

おしまい

CloudShell + CDK + GitHub の構成で IaC する方法を解説いたしました。皆様のお役に立てば幸いです。

もっといいやり方あるよ、といった情報あればぜひコメントで教えてください。ここまでご覧いただきありがとうございました。

脚注
  1. 何度実行しても同じ結果になる操作には「冪等性がある」と言います。冪等な操作のみからなる手続きは何度実行しても同じ結果になるので、手続的なアプローチで管理を行う場合には冪等性は非常に重要な概念となります。Ansible のような構成管理ツールも冪等性を重視しています。 ↩︎

  2. よしんば正気を保ったまま書き上げたとして、書き上げたコードを数ヶ月後にメンテナンスするときに発狂することになるでしょう。 ↩︎

  3. ちなみに今回紹介する方法で環境を構築した場合、node や CDK など合わせて 700MB くらいは消費するのでけっこうギリギリです。 ↩︎

  4. cdk init のときに実行される npm install はかなりの高確率でフリーズするか、実行できても異常に時間がかかります。 ↩︎

  5. K. Saito(X: @SightSeekerTw)様が解消方法をご共有くださいました。ありがとうございます。 ↩︎

  6. ツールのインストール方法は時間に伴って変わりますし、特定のバージョン以下では脆弱性が残っている可能性もあるので、公式ドキュメントに載っている最新の方法でインストールすることを強く推奨します。野良の記事に載っている方法は変な工夫や試行錯誤が加わっていることもあり、それが積み重なってシステムが破綻することもありますし、最悪の場合は脆弱性やマルウェアを仕込むように誘導されているかもしれません。執筆者にその気がなくとも typo などの凡ミスでそうなったり、又聞きを繰り返すうちにそうなる可能性もあります。本記事のような野良記事は参考程度までにとどめたほうがよいです。 ↩︎

  7. CloudShell ではどうなるか知りませんが、会社のパソコンで WSL 上に node の最新版をインストールしたらエラーが出て使えませんでした。将来的にそういった地雷を踏むことに怯えるよりは大人しく LTS 版を使っておくべきですね。 ↩︎

  8. npm install aws-cdk とすれば、実行したディレクトリの node_modules というディレクトリにインストールされますが、このやり方は使えないということです。グローバルを汚さないやり方が何かあるかもしれませんが、どう考えてもめちゃくちゃ面倒くさそうなので調べていません。 ↩︎

  9. 複数人開発でデプロイ用の共通アカウントを使用する場合など。 ↩︎

  10. Deploy Keys は使い回しができないので、複数のリポジトリを clone する必要がある場合はリポジトリごとに Host を変えてそれぞれの鍵を対応づけます。 ↩︎

  11. 私は VPC を作成したときにデフォルトで作成される NAT ゲートウェイに気づかず、50ドルくらい持っていかれました。 ↩︎

  12. 簡単に説明しておくと、root ユーザーで Cost Explorer へのアクセスをアクティブにして、AWSBillingConductor 系のポリシーを IAM ユーザーにアタッチします。 ↩︎

Discussion