è€æ°ã®AWSã¢ã«ãŠã³ãã«å¯ŸããŠãåããããªæ§æã®ä»çµã¿ãå°å ¥ããããšæã£ãããšã¯ãªãã§ããããïŒ
ä»åã¯èªåã管çè ãšãªããè€æ°ã®AWSã¢ã«ãŠã³ããž AWS CloudFormation (以äžãCloudFormation) ã§äœæãããªãœãŒã¹ãåãããã«æ§ç¯ããããŒãžã§ã³ã¢ããã¯ç®¡çè ãããŒãžã§ã³æ§æã¯AWSã¢ã«ãŠã³ãã®æ åœè ã®æ§æã«åºæ¥ãä»çµã¿ãäœæããŸãã
æ§æ
1ã€ã®å
ãšãªãã¢ã«ãŠã³ããããè€æ°ã®AWSã¢ã«ãŠã³ããž1ã€ã®è£œåãé
åžããŸãã
AWSãªãœãŒã¹ã®å®çŸ©ã¯ AWS Cloud Development Kit (以äžãAWS CDK) (TypeScript) ã§äœæããCloudFormation ãã³ãã¬ãŒãã«å€æããŸãããŸãé
åžãããªãœãŒã¹ã«ã¯ AWS Lambda ãå«ãŸããŠãããããã©ã³ã¿ã€ã ãEOLã«ãªã£ãæã«æŽæ°ã§ãããããªä»çµã¿ãå¿
èŠã§ãã

ã¡ãªã¿ã«ããã®ããã«ãã¹ã¿ãŒã¢ã«ãŠã³ããšåã¢ã«ãŠã³ãã®é¢ä¿ã§äžå 管çããæ¹æ³ããããã¢ã³ãã¹ããŒã¯ã¢ãã«ããšåŒã¶ããã§ãã
AWS Service Catalog ãšã¯ïŒ
AWS Service Catalog (以äžãService Catalog) ã¯ãäœæãã補åããšã³ããŠãŒã¶ãŒã«é åžã§ããæ©èœãæã€ãµãŒãã¹ã§ãã管çè ãå¿ èŠãªãŠãŒã¶ãŒã«ã®ã¿è£œåäœæã»é åžãè¡ãããšã³ããŠãŒã¶ãŒã¯ãã®è£œåã䜿çšããŠèªåã®ç°å¢ã§åãæ§æã®ãªãœãŒã¹ãæ§ç¯ããããšãã§ããŸãã
Service Catalog ã§åºãŠããçšèª
- 補å
åã¢ã«ãŠã³ãã«é åžããAWSãªãœãŒã¹ã§ãã
CloudFormationãã³ãã¬ãŒããTerraformãã³ãã¬ãŒããã¡ã€ã«ãªã©ãç»é²ããããšã§ãé åžããããšãå¯èœã«ãªããŸããããŒãžã§ã³ç®¡çãå¯èœã

- ããŒããã©ãªãª
補åãçŽã¥ããå ãããŒããã©ãªãªåäœã§ãŠãŒã¶ãŒã«è£œåã®äœ¿çšãèš±å¯ã§ããŸãã
ä»åã®å¥ã®AWSã¢ã«ãŠã³ããžã®é åžã§äœ¿ããŸããããã®ããŒããã©ãªãªã§ä»ã®AWSã¢ã«ãŠã³ããžã®å ±æèšå®ãããŠããŸãã
åæ
- é åžå ã®ã¢ã«ãŠã³ãã¯åºå®ãããŠããããé åžå ã®AWSã¢ã«ãŠã³ãã¯ã©ãã«ãªããéçºæã¯ææ¡ã§ããŠããªããã©ã®ãããã®AWSã¢ã«ãŠã³ãæ°ã«ãªãããäžæã ããè€æ°ã®AWSã¢ã«ãŠã³ããžé åžããããšã¯æ±ºãŸã£ãŠããã
- ãªãã¹ãé
åžå
ã®AWSã¢ã«ãŠã³ããžã®ãã°ã€ã³ã¯é¿ããããæäœã¯é
åžå
ã®AWSã¢ã«ãŠã³ãæ
åœè
ã«è¡ã£ãŠããããã
- ãã®ãã
cdk deployãé åžå ã®ã¢ã«ãŠã³ãã§å®è¡åºæ¥ãªããšããåæ
- ãã®ãã
課é¡
ãã®æ§æãäœæããã«ããããããã€ãã®èª²é¡ããããŸããã
â S3ãã±ãããåç §ã§ããªã
æåCDKã®ã³ãŒãã§ã¯ã以äžã®ããã«å®çŸ©ããŠããŸããã
const myFunction = new NodejsFunction(this, 'MyFunction', {
   runtime: lambda.Runtime.NODEJS_20_X,
   entry: path.join(__dirname, '../src/lambda/index.ts'),
   handler: 'handler',
  });
ãã®æãcdk.outã«åºåãããCloudFormationãã³ãã¬ãŒãã¯ä»¥äžã®ããã«ã¢ã«ãŠã³ãIDããªãŒãžã§ã³çããã¬ã€ã¹ãã«ããŒãšãªã£ãç¶æ ã§ãã
ã"XXXXHandler": {
  "Type": "AWS::Lambda::Function",
  "Properties": {
  "Code": {
   "S3Bucket": {
   "Fn::Sub": "cdk-xxxx-assets-${AWS::AccountId}-${AWS::Region}"
   },
   "S3Key": "{Hashå€}.zip"
  },
ããã¯Lambdaã®ã³ãŒããé
眮ãããã¢ã»ããæ
å ±ã瀺ããŠããŸãã
é
åžå
ã®ã¢ã«ãŠã³ãã¯ã©ãã«ãªããåãããªããšããåæããããããå
ã®ã¢ã«ãŠã³ãã§ã¢ã»ããã cdk deploy ã§ãããã€ããŸãã
ãã®ãŸãŸ Service Catalog ã§å¥ã¢ã«ãŠã³ããžé
åžãããšããã¬ã€ã¹ãã«ããŒéšåã«é
åžå
ã®AWSã¢ã«ãŠã³ããèšå®ããããããååšããŠããªã Amazon S3 (以äžãS3) ãã±ãããæ¢ãã«è¡ã£ãŠããŸããã¢ã¯ã»ã¹ãšã©ãŒãšãªããŸããïŒCloudFormationãã³ãã¬ãŒãã«èšèŒããããã®ã¯ãé
åžå
ã®AWSã¢ã«ãŠã³ãã§ãããã€ãããšãã®å€ã®ãããé
åžå
ã«ã¯ãã®S3ãã±ããã¯ååšããŸãããïŒ

ãã®ãã䜿çšããS3ãã±ããã«ãã±ããããªã·ãŒãèšå®ããã¯ãã¹ã¢ã«ãŠã³ãã§ã®åç §ãå¯èœã«ããå¿ èŠããããŸãã

課é¡â¡ éçšã³ã¹ããé«ããªã
ä»ã®ãŸãŸã§ã¯ä»¥äžã®å·¥çšãè¡ãå¿
èŠããããŸãã
1. cdk synth / cdk deploy ã§CloudFormationãã³ãã¬ãŒããååŸ
2. CloudFormationãã³ãã¬ãŒãã«ã¯S3ãã±ããã®åç
§å
ã«AWSã¢ã«ãŠã³ãããã¬ã€ã¹ãã«ããŒã§èšå®ãããŠãããããAWSã¢ã«ãŠã³ããé
åžå
ã®ã¢ã«ãŠã³ãã«åºå®ãã察å¿ãè¡ã
3. Service Catalogã«çœ®æããCloudFormationãã³ãã¬ãŒãã補åã«ç»é²ãã
æåã§ãã³ãã¬ãŒããæžãæããŠç»é²ããã®ã¯çŸå®çã§ã¯ãããŸããããªããšãèªååããããšããã§ãã
解決ç: CDKåŽã§Bootstrapçšã®ãã±ãããšã¯å¥ã«å ±æçšã®S3ãæå®ãã
è²ã 詊è¡é¯èª€ããŸããããCDKã®Bootstrapã§äœæãããS3ãã±ãããšã¯å¥ã«ãå ±æçšã®S3ãã±ãããäœæããããã«é åžããCloudFormationãã³ãã¬ãŒã(Asset)ãé 眮ããæ§æã«ããŸããã
CDKåŽã®èšå®
CDKåŽã§åºåãããCloudFormationãã³ãã¬ãŒãã®Lambdaã®Asset(S3ãã±ãã)ããã®å ±æãã±ããã«åºå®ããŸãããã®S3ãã±ããã«ã¯ãæåãã¹ã¯ãªãããäœæããŠã¯ãã¹ã¢ã«ãŠã³ãã®ãã±ããããªã·ãŒãä»äžããŠãããŸãã
// çç¥
this.bucket = new s3.Bucket(this, "SampleBucket", {
 encryption: s3.BucketEncryption.S3_MANAGED,
 blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
 versioned: true,
 enforceSSL: true,
 removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// é
åžå
ã®ã¢ã«ãŠã³ãã«å¯Ÿãããã±ããããªã·ãŒã®è¿œå
if (props.targetAccountIds && props.targetAccountIds.length > 0) {
 this.bucket.addToResourcePolicy(
  new iam.PolicyStatement({
   sid: "AllowCrossAccountRead",
   effect: iam.Effect.ALLOW,
   principals: props.targetAccountIds.map(
    (id) => new iam.AccountPrincipal(id)
   ),
   actions: ["s3:GetObject", "s3:GetObjectVersion", "s3:ListBucket"],
   resources: [this.bucket.bucketArn, `${this.bucket.bucketArn}/*`],
  })
 );
}
// Service CatalogããŒããã©ãªãª
const portfolio = new servicecatalog.Portfolio(this, "Portfolio", {
 displayName: "TestPortfolio",
 providerName,
});
// é
åžçšã®ã¢ã«ãŠã³ãã«å¯ŸããŠãããŒããã©ãªãªãžã®ã¢ã¯ã»ã¹æš©ãä»äž
targetAccountIds.forEach((accountId) => {
 portfolio.shareWithAccount(accountId);
});
é
åžå
ã§ã¯CDKã§ã¯ãªããCloudFormationãã³ãã¬ãŒããé
åžãããããCDKç¬èªã®æ
å ±ãå«ãŸããŠããŸããªãããã« generateBootstrapVersionRule: false ãã€ããŠããŸãã
import * as cdk from 'aws-cdk-lib';
import { SampleCdkStack } from '../lib/sample_cdk-stack';
const app = new cdk.App();
new SampleCdkStack(app, 'SampleCdkStack', {
synthesizer: new cdk.DefaultStackSynthesizer({
fileAssetsBucketName: 'sample-bucket'
generateBootstrapVersionRule: false,
})
});
åè: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.DefaultStackSynthesizerProps.html
ãã±ããããªã·ãŒã®èšå®
é
åžå
ã¢ã«ãŠã³ãïŒrootïŒã«å¯ŸããŠãs3:GetObject çã®æš©éãèš±å¯ããŸãã
â»ããS3ãã±ããã®æå·åããŒãKMSã«ããŠããå Žåã¯ãkms:Decrypt ã®æš©éãå¿
èŠã«ãªããŸãã
Amazon S3 ãã±ããã®ãµãŒããŒåŽã®æå·åããŒã倿Žãã
ããã©ã«ãã§ã¯ãããŒãã¹ãã©ããã¹ã¿ãã¯ã® Amazon S3 ãã±ããã¯ããµãŒããŒåŽã®æå·åã« AWS ãããŒãžãããŒã䜿çšããããã«èšå®ãããŠããŸããæ¢åã®ã«ã¹ã¿ããŒãããŒãžãããŒã䜿çšããã«ã¯ã âbootstrap-kms-key-idãªãã·ã§ã³ã䜿çšãã䜿çšãã AWS Key Management Service (AWS KMS) ããŒã®å€ãæå®ããŸããæå·åããŒããã詳现ã«å¶åŸ¡ãããå Žåã¯ãã«ã¹ã¿ããŒç®¡çããŒã䜿çšããããã« âbootstrap-customer-key ãæå®ããŸãã
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping-customizing.html
{
"Version": "2012-10-17",
"Id": "AccessControl",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::{ããã«ã¢ã«ãŠã³ãID}:root"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::{S3ãã±ããå}"
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::{ããã«ã¢ã«ãŠã³ãID}:root"
},
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::{S3ãã±ããå}/*"
}
]
}
CloudFormationãã³ãã¬ãŒãã¯ãLambdaã®ã³ãŒããšäžç·ã«æå®ããS3ãã±ããã«ã¢ããããŒããããŸãããã®æãcdk.out/cdk.out ã«èšè¿°ããããŸããããã¡ã€ã«ãããã·ã¥åãããŠããŸãã
"stackTemplateAssetObjectUrl": "s3://sample-bucket/{ããã·ã¥}.json",
æ€èšæ¡
Synthesizer ã§ã¢ã»ããããããã€ããå ã®S3ãã±ãããç¥ããªãã£ãæã«ãlambdaãäœæããèšè¿°ã§ãã±ãããæå®ããã°äžæããããšæã£ãŠããŸããã
const myFunction2 = new lambda.Function(this, 'MyFunction2', {
 runtime: lambda.Runtime.NODEJS_20_X,
 code: lambda.Code.fromBucket(s3.Bucket.fromBucketName(this, 'Bucket', 'share-bucket-cdk'), 'function.zip'), // ã³ãŒããããå Žæãæç€º
 handler: 'index.handler',
});
ããããã®å®çŸ©ã ãšãTypeScriptã§æžããŠããã³ãŒããã³ã³ãã€ã«ããæå®ããS3ã«é
眮ãããŸã§èªåã§è¡ããªããšãããªããªããŸãã
èªåã§ãã£ãŠããããã§ããããã£ããCDKã§L2䜿ã£ãŠæžããŠããã®ã«æçŸ©ããªããªã£ãŠããŸããããªæ°ãããŠãæ¡çšãèŠéããŸããã
ã³ãŒãã®æŽæ°ã«åããèªååã¹ã¯ãªããã§è£œåç»é²
ãããŸã§ã§1,2ã«é¢ããŠå¯Ÿå¿ã§ããŸããã
1. cdk synth / cdk deploy ã§CloudFormationãã³ãã¬ãŒããååŸ
2. CloudFormationãã³ãã¬ãŒãã«ã¯S3ãã±ããã®åç
§å
ã«AWSã¢ã«ãŠã³ãããã¬ã€ã¹ãã«ããŒã§èšå®ãããŠãããããAWSã¢ã«ãŠã³ããé
åžå
ã®ã¢ã«ãŠã³ãã®å
±æçšã«åºå®ãã察å¿ãè¡ã
3. Service Catalogã«çœ®æããCloudFormationãã³ãã¬ãŒãã補åã«ç»é²ãã
æ®ãã¯3ãèªååããã°ããªããšãéçšã§ãããã§ãã
ããããã®å
±æçšã®S3ãã±ããã«å
¥ã£ãŠãããã³ãã¬ãŒããService Catalogã«ã¢ããããŒãããéãService Catalog ã®è£œåãæªäœæãªãæ°èŠäœæãæ¢åãªã Provisioning Artifact ãšããŠæ°ããããŒãžã§ã³ã远å ããã®ãããªããšã¯ã§ããŸããã
ãã£ãŠãã³ãã¬ãŒãã®æžãæããš Service Catalog ãžã®ç»é²ãã·ã§ã«ã¹ã¯ãªããã§èªååããŸãã
ãã£ããã§ããã以äžã®ãããªã¹ã¯ãªãããæžããŠããŸãã
ã¹ã¯ãªããã®äž»ãªæµã
1. jq ã䜿ãããã³ãã¬ãŒãå
ã® S3Bucket åç
§ãå
±æãã±ããåã«çœ®æããŸã
2. æžãæãããã³ãã¬ãŒããå
±æS3ãžé
眮
3. aws servicecatalog create-provisioning-artifact ã§æ°ããŒãžã§ã³ãŸãã¯æ°ãã«è£œåãšããŠç»é²ã
#!/bin/bash
# æ±çšçãªç°å¢å€æ°ïŒå®éã¯CI/CDã®ç°å¢å€æ°ãªã©ããååŸïŒ
SHARE_BUCKET="your-shared-bucket-name"
TEMPLATE_FILE="cdk.out/SampleCdkStack.template.json"
TARGET_STACK_NAME="SampleProductTemplate"
PRODUCT_NAME="SampleAppProduct"
PRODUCT_VERSION="v1.0.0" # äŸ: Gitã®ã³ãããããã·ã¥ãæ¥ä»ã¿ã°
REGION="ap-northeast-1"
# 1. ã¢ã»ããã®åæ (Bootstrap -> Share) ã«é¢ããŠã¯çç¥ïŒaws s3 sync ãªã©ã§å®æœïŒ
echo "2. ãã³ãã¬ãŒãã®æžãæããšã¢ããããŒã"
# jqã䜿ã£ãŠLambda Codeã®S3Bucketåç
§ãå
±æãã±ããã«æžãæãã
jq --arg bucket "${SHARE_BUCKET}" '
walk(
if type == "object" and .S3Bucket? then
.S3Bucket = $bucket
else . end
)
' "${TEMPLATE_FILE}" > "${TEMPLATE_FILE}.tmp" && mv "${TEMPLATE_FILE}.tmp" "${TEMPLATE_FILE}"
# å
±æS3ãã±ãããžã¢ããããŒã
aws s3 cp "${TEMPLATE_FILE}" "s3://${SHARE_BUCKET}/${TARGET_STACK_NAME}.template.json" --region "${REGION}"
TEMPLATE_URL="https://${SHARE_BUCKET}.s3.${REGION}.amazonaws.com/${TARGET_STACK_NAME}.template.json"
echo "Service Catalog 補åã®ååšç¢ºèª"
# 補åãååšãããååã§æ€çŽ¢ããŠProductIDãååŸ
PRODUCT_ID=$(aws servicecatalog search-products-as-admin \
--region "${REGION}" \
--query "ProductViewDetails[?ProductViewSummary.Name=='${PRODUCT_NAME}'].ProductViewSummary.ProductId" \
--output text)
if [ -z "${PRODUCT_ID}" ]; then
echo "3. 補åãååšããªããããæ°èŠäœæããŸã"
aws servicecatalog create-product \
--name "${PRODUCT_NAME}" \
--owner "IT_Admin" \
--product-type "CLOUDFORMATION_TEMPLATE" \
--provisioning-artifact-parameters "Name=${PRODUCT_VERSION},Info={LoadTemplateFromURL=${TEMPLATE_URL}},Type=CLOUD_FORMATION_TEMPLATE" \
--region "${REGION}"
else
echo "4. 補åãæ¢ã«ååšãããããæ°ããããŒãžã§ã³ãšããŠè¿œå ããŸã"
aws servicecatalog create-provisioning-artifact \
--product-id "${PRODUCT_ID}" \
--parameters "Name=${PRODUCT_VERSION},Info={LoadTemplateFromURL=${TEMPLATE_URL}},Type=CLOUD_FORMATION_TEMPLATE" \
--region "${REGION}"
fi
ãã®åŸã®é
åžã®å¯Ÿå¿ã¯ã¹ã¯ãªããã§æžããšç
©éã«ãªããããæåã§è¡ãããã«ããŠããŸãã
ããã§CDKã§äœæããæ§æããå¥ã®AWSã¢ã«ãŠã³ãã«Service Catalogçµç±ã§é
åžããããšãå®çŸã§ããŸããã
ãŸãšã
æçµçã«ã¹ã¯ãªããã«ããå¶åŸ¡ãå¿
èŠã«ãªããŸããããCDKã§äœæããæ§æã Service Catalog çµç±ã§ã¹ããŒãã«é
åžããä»çµã¿ãå®çŸã§ããŸããã
ãã«ãã¢ã«ãŠã³ã管çãã瀟å
æšæºã€ã³ãã©ã®ã«ã¿ãã°åãæ€èšããŠããæ¹ã®åèã«ãªãã°å¹žãã§ããïŒä»ã«ããããæ¹ãããã°ããã²æããŠãã ããïŒïŒ
åè
AWS Service Catalog Hub and Spoke Model: How to Automate the Deployment and Management of Service Catalog to Many Accounts
AWS Service Catalog ã䜿ã£ãã»ã«ããµãŒãã¹åæ©èœã®æäŸ
AWS CDK ã䜿çšã㊠AWS Service Catalog ããŒããã©ãªãªãšè£œåã®ãããã€ãèªååãã
CDK ã¹ã¿ãã¯åæãã«ã¹ã¿ãã€ãºãã