はじめに
踏み台サーバーではEC2サーバー+SSM Session Managerを使うのが最も主流(だと思う)が、Fargateで作る手段も存在する(Fargate Bastion)。
以下の書籍でもハンズオンの中で記載されている。※ECS使うならこの本必読だと思う。
ECS/Fargateで主要な部分を構築するが踏み台だけEC2みたいな状況が度々発生していたので、AWS CDKを使用してFargate Bastionにチャレンジしてみた。
なおコンテナ関連の資材については上記書籍の資材(以下)を使用させていただいた。
https://github.com/uma-arai/sbcntr-resources/tree/main/fargate-bastion
成果物
全量は以下にある。
アーキテクチャ
全体像は以下である。
以下ポイントのみ記載。
- Fargate Bastionはプライベートサブネットで起動。VPCエンドポイントを介して各種サービスと通信。
- Fargate Bastionはサービスを介して起動(タスク定義から直接起動ではなく)。普段はタスク数0としておき、使う場合にタスク数を1以上とするイメージ。
- コンテナイメージをビルド、デプロイするCodePipelineも作成。
CDKの実装について特に考慮した点など記載
全般
スタックに定義を書くと肥大化して可読性が下がるため、constructsで構造化している。
具体的には以下のようにconstructs
配下に概ねリソースの種類ごとに定義を作成し、それをもとにスタック上でリソースを生成する形とした。
├── fargateBastionStack.ts // 1スタック構成 └── constructs ├── AuroraConstruct.ts // Auroraの定義 ├── BastionCodepipelineConstruct.ts // CodePipelineとそれに付随するリソースの定義 ├── BastionEcsConstruct.ts // ECSとそれに付随するリソースの定義 ├── EcrConstruct.ts // ECRの定義 ├── SecurityGroupConstruct.ts // セキュリティグループとVPCエンドポイントの定義 ├── config │ └── buildspecConfig.ts // CodeBuildにおけるビルドスペックを取得する関数の定義 ├── docker // 踏み台サーバー用の資材。参考資料[3]の資材を参考にさせていただいた。 │ ├── Dockerfile // 踏み台サーバー用のDockerfile │ └── run.sh // 踏み台サーバーで使用するスクリプト └── vpcConstruct.ts // VPCとサブネットの定義
Aurora関連
あくまで動作確認用のDBクラスターであり、特記事項はないためカツいあい。
CodePipeline関連
CodeCommitは事前に手動で作成して資材をプッシュしておき、CDK上では参照するのみとしている。
CDK上でCodeCommitリポジトリも作成し、資材も自動でアップロードする方法などもあるが、Fargate Bastionの資材は更新頻度が低い想定なので今回はそこまで頑張らないことにした。
//--------------CodeCommit---------------- // コードのリポジトリは手動作成したものを読み込み const bastionCodeRepository = codecommit.Repository.fromRepositoryName(scope, 'BastionRepository', 'BastionRepository')
上記以外はオーソドックスなECSにおけるCICDパイプラインの構成となっている。
Fargate関連(ECS, ECR含む)
vCPUとメモリについては以下の通り最低限としている。
const bastionTaskDefinition = new ecs.TaskDefinition(scope, 'BastionTaskDefinition', { compatibility: ecs.Compatibility.FARGATE, cpu: '256', memoryaB: '512', family: 'fargate-bastion-task-definition' })
またタスク定義において、DBの接続情報をSecrets Manager経由で環境変数で注入する形にしている。 これによりFargate Bastionに接続した際に、環境変数をもとに接続情報の確認やDBの接続が可能。 ※ただしこれだとログにパスワードが残ってしまう可能性があるのでそこは注意。
// タスク定義にコンテナを追加 bastionTaskDefinition.addContainer('BastionApp', { containerName: ecrResources.bastionEcrRepository.repositoryName, image: ecs.ContainerImage.fromEcrRepository(ecrResources.bastionEcrRepository, 'latest'), secrets: { 'DB_HOST': ecs.Secret.fromSecretsManager(dbSecrets, 'host'), 'DB_NAME': ecs.Secret.fromSecretsManager(dbSecrets, 'dbname'), 'DB_USERNAME': ecs.Secret.fromSecretsManager(dbSecrets, 'username'), 'DB_PASSWORD': ecs.Secret.fromSecretsManager(dbSecrets, 'password'), }, logging: bastionLogging })
またFargate Bastionを実現するためには以下のようにSSM関連のポリシーをタスクロールに付与する、SSM用のロールを作成する等が必要。 ここは参考資料や書籍で解説されているところであるため詳細は割愛する。
// タスクロールに付与するポリシーを作成 const bastionTaskRolePolicyStatement = new iam.Policy(scope, 'BastionSSMPolicy', { statements: [ // ECSタスクがSSMへ権限を渡すためのPassRole new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['iam:PassRole'], resources: ['*'], conditions: { 'StringEquals': { 'iam:PassedToService': 'ssm.amazonaws.com' } } }), // ECSタスク内でSSMのアクティベーションを発行するための権限 new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'ssm:DeleteActivation', 'ssm:RemoveTagsFromResource', 'ssm:AddTagsToResource', 'ssm:CreateActivation' ], resources: ['*'] }) ], }) // タスクロールに作成したポリシー付与 bastionTaskDefinition.taskRole.attachInlinePolicy(bastionTaskRolePolicyStatement) // ECSタスクがSSMに渡すIAMロールを作成 const bastionSSMserviceRole = new iam.Role(scope, 'SsmRoleForBastion', { assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'), description: 'IAM Role for passing from ECS Task to SSM', roleName: 'BastionSSMServiceRole' }) //SSMが使用するポリシーを付与 bastionSSMserviceRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'))
ECSサービスにおいて以下のようにタスク数0を指定している。これによりデプロイ時はタスク数0となる。 また今回はECS Execは有効化していない。
//--------------ECSサービスの生成---------------- this.bastionService = new ecs.FargateService(scope, 'BackendService', { cluster: bastionCluster, //enableExecuteCommand: false, // ECS Execを有効化したい場合はここをtrueにして必要な権限を付与 taskDefinition: bastionTaskDefinition, desiredCount: 0, // タスク数0を指定 minHealthyPercent: 100, maxHealthyPercent: 200, deploymentController: { type: ecs.DeploymentControllerType.ECS }, circuitBreaker: { rollback: true }, securityGroups: [securityGroupResources.bastionSg], vpcSubnets: { subnets: [vpcResources.subnetContainer1a, vpcResources.subnetContainer1c] }, })
VPC, Subnet関連
VPC関連では以下2つを意識的に実施。
- NATゲートウェイを作らないように明示的に0を指定。デフォルトだと作られてしまうので注意(これ引っかかりやすいトラップな気がする)
- SubnetはCidrBlockを指定したいケースが多いため、VPC作成時には定義せず後続でそれぞれ定義。
// VPCを作成 this.vpc = new ec2.Vpc(scope, 'VPC', { natGateways: 0, // デフォルトでは1だが不要なので0をセット maxAzs: 2, // AZの数を2に指定 cidr: '10.0.0.0/16', subnetConfiguration: [], // デフォルトではAZごとにSubnetが作られてしまうため、明示的に作らないよう指定 enableDnsHostnames: true, enableDnsSupport: true, vpcName: 'fargate-bastion-vpc' } )
使い方など
- CodeCommitのリポジトリを手動で作成する(リポジトリ名は
BastionRepository
にする。変更する場合はCDKのハードコード箇所を修正する)。 ./lib/constructs/docker/
配下の資材を上記リポジトリにプッシュ。- Systems Managerでアドバンスドインスタンスティアへの変更をしておく。
- ECSサービスで踏み台のタスクを起動する。
- Systems Manager -> セッションマネージャーで対象のタスクを選択して接続。※
- DBの接続情報は環境変数で読み込んでいるので、以下で接続可能。パスワードは
env|grep DB_PASSWORD
で確認する。mysql -h $DB_HOST -u $DB_USERNAME --password=$DB_PASSWORD
※で接続先のタスクを探すときに、古いタスクなども出てきてしまう(?)ためか、一見どれか分からない。
これはタスク起動時のログでインスタンスID mi-xxxxxx
が出力されるので、それをもとに一覧から対象を特定できる。
接続先のタスク(ターゲットインスタンス)はログに出力されたIDをもとに特定する。
あとは通常の踏み台のように使用可能。以下の通りDBにも接続できる。
その他
これでEC2ゼロにできる(といいな)。
ECS本は本当おすすめです(再掲)。