AWS SAMはAWS公式のサーバーレスアプリケーション構築用のオープンソースフレームワークで、サーバレスアプリケーションを作成するための便利な機能が実装されています。AWS SAMでCI/CDパイプラインも簡単に構築できますが、CodeCommitを別のAWSアカウントに設置する場合は一工夫が必要でした。
また、AWS SAMで自動生成されるCI/CDパイプラインはさまざまなケースに対応できるように冗長な構成となっています。不要な機能をバッサリ削除してシンプルに、かつ弊社の運用では汎用となるような構成となるように改変しました。
サンプルアプリケーション
この記事では次のような2つの機能を持った簡単なAPIを実装し、CI/CDパイプラインを構築します。
- 住所を「都道府県」/「市区町村」/「町域以下」の3つの部分に分割
- 郵便番号から住所を返す
日本郵便の郵便番号データ「住所の郵便番号(1レコード1行、UTF-8形式)」をDynamoDBへ保存し、APIで利用します。
API実装
sam init
から開始し、PythonでAPIを実装しました。APIの実装についてはこの記事の本筋とは外れますので、概要の説明と主要コードの掲載のみとします。
DynamoDBには、郵便番号データを次のようなフィールドで保存しています。DynamoDBへのデータインポート方法の説明は、この記事では省略します。
{
"zipcode": "0640941", # 郵便番号
"prefcode": "01", # 都道府県コード(JIS X 0401)
"address1": "北海道", # 都道府県
"address2": "札幌市中央区", # 市区町村
"address3": "旭ケ丘", # 町域
"kana1": "ホッカイドウ", # 都道府県カナ
"kana2": "サッポロシチュウオウク", # 市区町村カナ
"kana3": "アサヒガオカ", # 町域カナ
"first2": "札幌" # 市区町村最初の2文字
}
DynamoDBのパーティションキーはzipcodeとしています。
グローバルセカンダリインデックス(キー名: prefcode-first2-index)として、パーティションキーprefcode, ソートキー first2を設定しています。日本の市区町村が必ず2文字以上あるということから市区町村の最初の2文字をDynamoDBでインデックスし、検索に利用しています。DynamoDBのScanではなくQueryを使うことにより、高速に効率よく住所の候補を検索することができます。
APIのQuery Stringにzipcode引数が与えられた場合は、DynamoDBからzipcodeを元に住所情報を取得して返します。
address引数が与えられた場合は、DynamoDBから都道府県コードと市区町村の最初の2文字を元に住所の候補を取得し、候補の中で、一番長くマッチする住所情報を探して返します。
実行例1 (郵便番号から住所を検索)
URL: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/search?zipcode=1600023
{
"message": null,
"results": [
{
"zipcode": "1600023",
"prefcode": "13",
"address1": "東京都",
"address2": "新宿区",
"address3": "西新宿",
"kana1": "トウキョウト",
"kana2": "シンジュクク",
"kana3": "ニシシンジュク"
}
],
"status": 200
}
実行例2 (住所を都道府県/市区町村/町域以下に分割)
URL: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/search?address=東京都新宿区西新宿1丁目22-15%20グラフィオ西新宿9F
{
"message": null,
"results": {
"zipcode": "1600023",
"prefcode": "13",
"address1": "東京都",
"address2": "新宿区",
"address3": "西新宿1丁目22-15 グラフィオ西新宿9F",
"kana1": "トウキョウト",
"kana2": "シンジュクク",
"kana3": "ニシシンジュク"
},
"status": 200
}
APIのコード(python/app.py)は以下のようになります。サンプルなので、エラー処理等がいいかげんです。
import json
import logging
import os
import boto3
import botocore
TABLENAME = os.getenv("TABLENAME", default="ZipCode")
# 都道府県コード(JIS X 0401)
prefs = {
"北海道": "01", "青森県": "02", "岩手県": "03", "宮城県": "04",
"秋田県": "05", "山形県": "06", "福島県": "07", "茨城県": "08",
"栃木県": "09", "群馬県": "10", "埼玉県": "11", "千葉県": "12",
"東京都": "13", "神奈川県": "14", "新潟県": "15", "富山県": "16",
"石川県": "17", "福井県": "18", "山梨県": "19", "長野県": "20",
"岐阜県": "21", "静岡県": "22", "愛知県": "23", "三重県": "24",
"滋賀県": "25", "京都府": "26", "大阪府": "27", "兵庫県": "28",
"奈良県": "29", "和歌山県": "30", "鳥取県": "31", "島根県": "32",
"岡山県": "33", "広島県": "34", "山口県": "35", "徳島県": "36",
"香川県": "37", "愛媛県": "38", "高知県": "39", "福岡県": "40",
"佐賀県": "41", "長崎県": "42", "熊本県": "43", "大分県": "44",
"宮崎県": "45", "鹿児島県": "46", "沖縄県": "47"
}
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def from_dynamodb_item(item):
ret = {}
for key in item:
ret[key] = item[key]["S"]
return ret
def search(zipcode):
try:
dynamodb = boto3.client("dynamodb")
response = dynamodb.query(
TableName=TABLENAME,
KeyConditionExpression="zipcode = :zipcode",
ExpressionAttributeValues={
":zipcode": {"S": zipcode}
}
)
except botocore.exceptions.ClientError as err:
if err.response["Error"]["Code"] == "LimitExceededException":
logger.warning("API Limit exceeded")
else:
raise err
if "Items" in response and len(response["Items"]) > 0:
items = []
for item in response["Items"]:
decoded = from_dynamodb_item(item)
del decoded["first2"]
items.append(decoded)
return items
return None
def longest_match(rest, items):
longest = None
longest_len = 0
for item in items:
decoded = from_dynamodb_item(item)
del decoded["first2"]
addr23 = decoded["address2"] + decoded["address3"]
length = len(addr23)
if longest_len < length and addr23 == rest[0:length]:
longest = decoded
longest["address3"] = rest[len(decoded["address2"]):]
longest_len = length
continue
length = len(decoded["address2"])
if longest_len < length and decoded["address2"] == rest[0:length]:
longest = decoded
longest["address3"] = rest[len(decoded["address2"]):]
longest_len = length
return longest
def match(address):
try:
prefcode = None
first2 = None
rest = None
for pref, prefcode in prefs.items():
if address[0:len(pref)] == pref:
rest = address[len(pref):]
first2 = rest[0:2]
break
if prefcode is None:
raise ValueError
dynamodb = boto3.client("dynamodb")
response = dynamodb.query(
TableName=TABLENAME,
IndexName="prefcode-first2-index",
KeyConditionExpression="prefcode = :prefcode AND first2 = :first2",
ExpressionAttributeValues={
":prefcode": {"S": prefcode},
":first2": {"S": first2}
}
)
except botocore.exceptions.ClientError as err:
if err.response["Error"]["Code"] == "LimitExceededException":
logger.warning("API Limit exceeded")
else:
raise err
if "Items" in response and len(response["Items"]) > 0:
matched = longest_match(rest, response["Items"])
return matched
return None
def lambda_handler(event, context):
logger.info("Received event: %s", json.dumps(event, indent=2))
logger.info(context)
try:
if "queryStringParameters" not in event:
raise ValueError
params = event["queryStringParameters"]
if "zipcode" in params:
results = search(params["zipcode"])
elif "address" in params:
results = match(params["address"])
else:
raise ValueError
if results is None:
results = []
return {
"statusCode": 200,
"body": json.dumps({
"message": None,
"results": results,
"status": 200
}),
"headers": {
"Content-Type": "application/json"
}
}
except ValueError:
return {
"statusCode": 400,
"body": json.dumps({
"message": "Invalid parameters.",
"results": None,
"status": 400
}),
"headers": {
"Content-Type": "application/json"
}
}
AWSリソース定義のtemplate.yamlは以下のようになります。DynamoDBキャパシティユニットのオートスケールを設定しているため記述が長くなってしまっていますが、オートスケールの部分(98行目からのDynamoDBScalingRoleから下すべて)を除けばシンプルな構成となっています。
Lambda実行ログを保存するCloudWatch Logsロググループのログ保持期間を設定するため、ロググループを明示的に作成しています。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
zipcode
Globals:
Function:
Timeout: 30
Parameters:
Stage:
Description: Deploy Stage
Type: String
Default: Staging
AllowedValues:
- Staging
- Production
MinCapacityUnit:
Description: DynamoDB Minimum Capacity Unit
Type: Number
Default: 1
MaxCapacityUnit:
Description: DynamoDB Maximum Capacity Unit
Type: Number
Default: 10
FunctionLogRetentionInDays:
Type: Number
Default: 7
Conditions:
IsProduction: !Equals [ !Ref Stage, Production ]
Resources:
ZipCodeFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: python/
Handler: app.lambda_handler
Runtime: python3.10
Architectures:
- x86_64
Events:
ZipCode:
Type: Api
Properties:
Path: /search
Method: GET
Environment:
Variables:
TABLENAME: !Ref ZipCodeDynamoDB
Policies:
- DynamoDBReadPolicy:
TableName: !Ref ZipCodeDynamoDB
- DynamoDBWritePolicy:
TableName: !Ref ZipCodeDynamoDB
ZipCodeFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ZipCodeFunction}"
RetentionInDays: !Ref FunctionLogRetentionInDays
ZipCodeDynamoDB:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: zipcode
AttributeType: S
- AttributeName: prefcode
AttributeType: S
- AttributeName: first2
AttributeType: S
GlobalSecondaryIndexes:
- IndexName: prefcode-first2-index
KeySchema:
- AttributeName: prefcode
KeyType: HASH
- AttributeName: first2
KeyType: RANGE
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: !Ref MinCapacityUnit
WriteCapacityUnits: !Ref MinCapacityUnit
KeySchema:
- AttributeName: zipcode
KeyType: HASH
- AttributeName: first2
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: !Ref MinCapacityUnit
WriteCapacityUnits: !Ref MinCapacityUnit
TableName: !If
- IsProduction
- ZipCode
- !Ref AWS::NoValue
DynamoDBScalingRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: application-autoscaling.amazonaws.com
Action: sts:AssumeRole
Path: "/"
Policies:
- PolicyName: DynamoDBAutoScale
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:DescribeTable"
- "dynamodb:UpdateTable"
- "cloudwatch:PutMetricAlarm"
- "cloudwatch:DescribeAlarms"
- "cloudwatch:GetMetricStatistics"
- "cloudwatch:SetAlarmState"
- "cloudwatch:DeleteAlarms"
Resource: "*"
DynamoDBWriteCapacityScalableTarget:
Type: "AWS::ApplicationAutoScaling::ScalableTarget"
Properties:
MaxCapacity: !Ref MaxCapacityUnit
MinCapacity: !Ref MinCapacityUnit
ResourceId: !Sub "table/${ZipCodeDynamoDB}"
RoleARN: !GetAtt DynamoDBScalingRole.Arn
ScalableDimension: "dynamodb:table:WriteCapacityUnits"
ServiceNamespace: dynamodb
DynamoDBWriteScalingPolicy:
Type: "AWS::ApplicationAutoScaling::ScalingPolicy"
Properties:
PolicyName: WriteAutoScalingPolicy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref DynamoDBWriteCapacityScalableTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70
ScaleInCooldown: 60
ScaleOutCooldown: 60
PredefinedMetricSpecification:
PredefinedMetricType: DynamoDBWriteCapacityUtilization
DynamoDBReadCapacityScalableTarget:
Type: "AWS::ApplicationAutoScaling::ScalableTarget"
Properties:
MaxCapacity: !Ref MaxCapacityUnit
MinCapacity: !Ref MinCapacityUnit
ResourceId: !Sub "table/${ZipCodeDynamoDB}"
RoleARN: !GetAtt DynamoDBScalingRole.Arn
ScalableDimension: "dynamodb:table:ReadCapacityUnits"
ServiceNamespace: dynamodb
DynamoDBReadScalingPolicy:
Type: "AWS::ApplicationAutoScaling::ScalingPolicy"
Properties:
PolicyName: ReadAutoScalingPolicy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref DynamoDBReadCapacityScalableTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70
ScaleInCooldown: 60
ScaleOutCooldown: 60
PredefinedMetricSpecification:
PredefinedMetricType: DynamoDBReadCapacityUtilization
DynamoDBPrefcodeWriteCapacityScalableTarget:
Type: "AWS::ApplicationAutoScaling::ScalableTarget"
Properties:
MaxCapacity: !Ref MaxCapacityUnit
MinCapacity: !Ref MinCapacityUnit
ResourceId: !Sub "table/${ZipCodeDynamoDB}/index/prefcode-first2-index"
RoleARN: !GetAtt DynamoDBScalingRole.Arn
ScalableDimension: "dynamodb:index:WriteCapacityUnits"
ServiceNamespace: dynamodb
DynamoDBPrefcodeWriteScalingPolicy:
Type: "AWS::ApplicationAutoScaling::ScalingPolicy"
Properties:
PolicyName: PrefcodeWriteAutoScalingPolicy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref DynamoDBPrefcodeWriteCapacityScalableTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70
ScaleInCooldown: 60
ScaleOutCooldown: 60
PredefinedMetricSpecification:
PredefinedMetricType: DynamoDBWriteCapacityUtilization
DynamoDBPrefcodeReadCapacityScalableTarget:
Type: "AWS::ApplicationAutoScaling::ScalableTarget"
Properties:
MaxCapacity: !Ref MaxCapacityUnit
MinCapacity: !Ref MinCapacityUnit
ResourceId: !Sub "table/${ZipCodeDynamoDB}/index/prefcode-first2-index"
RoleARN: !GetAtt DynamoDBScalingRole.Arn
ScalableDimension: "dynamodb:index:ReadCapacityUnits"
ServiceNamespace: dynamodb
DynamoDBPrefcodeReadScalingPolicy:
Type: "AWS::ApplicationAutoScaling::ScalingPolicy"
Properties:
PolicyName: PrefcodeReadAutoScalingPolicy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref DynamoDBPrefcodeReadCapacityScalableTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70
ScaleInCooldown: 60
ScaleOutCooldown: 60
PredefinedMetricSpecification:
PredefinedMetricType: DynamoDBReadCapacityUtilization
AWS SAMでCI/CDパイプラインを作成
AWS SAMの機能を使ってCI/CDパイプラインを作成します。sam pipeline init --bootstrap
を実行して質問に回答していくと、CodePipelineと関連するリソースを作成するCloudFormationテンプレートcodepipeline.yamlが自動生成されます。
今回は、CodeCommitとCodePipelineを使った2-stage Pipelineを作成しました。1ステージ目の名称はstaging、2ステージ目の名称はproductionとしています。ステージを設定する過程で(–bootstrap引数の効果)、ふたつのCloudFormationスタック(aws-sam-cli-managed-staging-pipeline-resources, aws-sam-cli-managed-production-pipeline-resources)が自動生成されます。
生成されたcodepipeline.yamlをCloudFormationで適用すると(sam deploy -t codepipeline.yaml --stack-name ZipCodePipeline --capabilities=CAPABILITY_IAM
)、最終的に次のようなリソースが生成されます。
CodeCommitを別AWSアカウントへ
sam pipeline
initで生成された構成から、CodeCommitを別のAWSアカウントへ移動します。
AWS SAMで自動生成されるパイプラインは、さまざまな構成で使用できるよう、冗長な部分があります。使わない部分を削ってシンプルな構成にします。
- CodePipeline実行アカウントとデプロイ先(Test/Prod)のAWSアカウントを全て別のアカウントにするパターンは不要
- Artifact保存用S3 Bucketが3つあるため、ひとつにまとめる
- S3 Bucketのアクセスログ(参考リンク)は不要なので全て削除
- デプロイ時のロールが冗長なのでまとめる
- FeatureBranch、CodeStar、Lambda Imageに関する機能は不要なので削除
また、AWS SAMの不具合と思われる部分を修正します。
- CodeBuildServiceRoleに「DependsOn: PipelineStackCloudFormationExecutionRole」を追加(これがないと、パイプラインのCloudFormationスタックを削除する時にエラーが発生する場合がある)
- CodePipelineの「UpdatePipelineステージ」CreateChangeSetのParameterOverridesで一部しかパラメータを渡していないため、全パラメータをフルで渡すように修正
上記を施した構成は次のようになります。
この構成のポイントは以下のとおりです。
- CodeCommitを配置するアカウント(RepositoryAccount)とCodePipelineを配置、APIをデプロイするアカウント(PipelineAccount)を別のAWSアカウントとします
- CodeCommitの状態変更は、Pipelineアカウント内のカスタムイベントバス(名称: codecommit)に転送されます
- PipelineAccount側のEventルール(名称: CloudWatchEventRule)がカスタムイベントバスcodecommitを監視し、mainブランチへの更新を検知してCodePipelineを起動します
- CodePipelineはRepositoryAccount側のIAMロール(名称: CodeCommitRole)にAssumeRoleすることが可能で、このロールの権限でCodeCommitのデータにアクセスします
- CodeCommitRoleはPipelineAccount側のS3 Bucket(名称: PipelineArtifactsBucket)およびKMS Keyへのアクセス権限を持つため、CodePipelineの求めに応じてソースコードのアーカイブをPipelineArtifactsBucketへ保存可能です
- CodeCommitとCodePipelineを別のAWSアカウントに設置する構成の場合、S3 Bucketの暗号化はデフォルトのキーを使えず、カスタムキーで実施しなければならないようなので、KMSキーを作成しています
- CodePipeline実行の過程で生成される中間生成物は(CodeBuildから起動されるsamコマンドの生成物も含め)、全てPipelineArtifactsBucketに保存されます
- samを実行する権限、sam deployでCloudFormationを実行する権限はそれぞれPipelineExecutionRole, CloudFormationExecutionRoleが使用され、第1ステージ、第2ステージで共通です
この構成のリソースの作成は、次に説明するように3つのCloudFormationテンプレートで行います。
CodeCommitと関連リソース
RepositoryAccountでCodeCommit関連のリソースを作成します。CloudFormationテンプレートcodecommit.yamlを適用して作成します。リポジトリひとつにつき、CloudFormationスタックをひとつ作成します。今回はリポジトリがひとつなので、CloudFormationスタックをひとつ作成します。
パラメータにはリポジトリ名(RepositoryName
)などの他に、CodePipelineを設置するAWSアカウントIDをPipelineAccountId
に与えます。
AWSTemplateFormatVersion: 2010-09-09
Description: "CodeCommit for cross-account pipeline"
Parameters:
RepositoryName:
Description: Repository Name
Type: String
RepositoryDescription:
Description: Repository Description
Type: String
MainBranchName:
Description: Main Branch Name of the Repository
Type: String
Default: main
PipelineAccountId:
Description: Account ID of the Pipeline
Type: String
Default: 123456789012
EventBusName:
Description: Event Bus name for CodeCommit state change notification
Type: String
Default: codecommit
Resources:
CodeCommitRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Ref RepositoryName
RepositoryDescription: !Ref RepositoryDescription
EventRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.codecommit
detail-type:
- CodeCommit Repository State Change
resources:
- !GetAtt CodeCommitRepository.Arn
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- !Ref MainBranchName
Targets:
- Arn: !Sub "arn:aws:events:${AWS::Region}:${PipelineAccountId}:event-bus/${EventBusName}"
RoleArn: !GetAtt EventRole.Arn
Id: !Sub "codepipeline-AppPipeline"
EventRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${RepositoryName}-EventRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: EventsBus
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: events:PutEvents
Resource: !Sub "arn:aws:events:${AWS::Region}:${PipelineAccountId}:event-bus/${EventBusName}"
CodeCommitRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- !Sub arn:aws:iam::${PipelineAccountId}:root
Action:
- sts:AssumeRole
Path: /
Policies:
- PolicyName: source
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:PutObjectAcl
Resource: "*"
- Effect: Allow
Action:
- kms:DescribeKey
- kms:GenerateDataKey*
- kms:Encrypt
- kms:ReEncrypt*
- kms:Decrypt
Resource: "*"
- Effect: Allow
Action:
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:UploadArchive
- codecommit:GetUploadArchiveStatus
- codecommit:CancelUploadArchive
Resource: "*"
Outputs:
RepositoryUrl:
Description: URL to use for cloning the repository over SSH
Value: !GetAtt CodeCommitRepository.CloneUrlSsh
CodeCommitRole:
Description: Role ARN for accessing CodeCommit
Value: !GetAtt CodeCommitRole.Arn
SAMパイプライン共通リソース
PipelineAccountでS3 BucketやIAMロールなどの権限を作成します。このプロジェクトだけではなく、他のプロジェクトでもこのリソースを利用することができますので、AWSアカウントにつき一回だけ、下記のCloudFormationテンプレートを適用してリソースを作成します。
CloudFormationのパラメータには、CodeCommitを設置したRepositoryAccountのAWSアカウントIDを指定します。
このCloudFormationテンプレートはsam pipeline init --bootstrap
で自動作成されるCloudFormationスタック(aws-sam-cli-managed-staging-pipeline-resources, aws-sam-cli-managed-production-pipeline-resources)のテンプレートを参考に作成しました。
AWSTemplateFormatVersion: '2010-09-09'
Description: "SAM Codepipeline resources"
Parameters:
RepositoryAccountId:
Description: AWS Account ID of the repository
Type: String
Default: 987654321098
EventBusName:
Description: Event Bus name for CodeCommit state change notification
Type: String
Default: codecommit
Resources:
PipelineUser:
Type: AWS::IAM::User
Properties:
Policies:
- PolicyName: AssumeRoles
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "sts:AssumeRole"
Resource: "*"
Condition:
StringEquals:
aws:ResourceTag/Role: pipeline-execution-role
PipelineUserAccessKey:
Type: AWS::IAM::AccessKey
Properties:
Serial: 1
Status: Active
UserName: !Ref PipelineUser
PipelineUserSecretKey:
Type: AWS::SecretsManager::Secret
Properties:
SecretString: !Sub '{"aws_access_key_id": "${PipelineUserAccessKey}", "aws_secret_access_key": "${PipelineUserAccessKey.SecretAccessKey}"}'
CloudFormationExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: GrantCloudFormationFullAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: '*'
Resource: '*'
PipelineExecutionRole:
Type: AWS::IAM::Role
Properties:
Tags:
- Key: Role
Value: pipeline-execution-role
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS: !GetAtt PipelineUser.Arn
Action:
- 'sts:AssumeRole'
- Effect: Allow
Principal:
# Allow roles with tag Role=aws-sam-pipeline-codebuild-service-role to assume this role.
# This is required when CodePipeline is the CI/CD system of choice.
AWS: !Ref AWS::AccountId
Action:
- 'sts:AssumeRole'
Condition:
StringEquals:
aws:PrincipalTag/Role: aws-sam-pipeline-codebuild-service-role
ArtifactsBucket:
Type: AWS::S3::Bucket
ArtifactsBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ArtifactsBucket
PolicyDocument:
Statement:
- Effect: "Deny"
Action: "s3:*"
Principal: "*"
Resource:
- !Join [ '',[ !GetAtt ArtifactsBucket.Arn, '/*' ] ]
- !GetAtt ArtifactsBucket.Arn
Condition:
Bool:
aws:SecureTransport: false
- Effect: "Allow"
Action:
- 's3:GetObject*'
- 's3:PutObject*'
- 's3:GetBucket*'
- 's3:List*'
Resource:
- !Join ['',[!GetAtt ArtifactsBucket.Arn, '/*']]
- !GetAtt ArtifactsBucket.Arn
Principal:
AWS:
- !GetAtt PipelineExecutionRole.Arn
- !GetAtt CloudFormationExecutionRole.Arn
- !Sub arn:aws:iam::${RepositoryAccountId}:root
ArtifactsBucketKey:
Type: AWS::KMS::Key
Properties:
Description: Encription key for Artifacts Bucket
KeyPolicy:
Version: 2012-10-17
Id: key-default-1
Statement:
- Sid: Enable IAM User Permissions
Effect: Allow
Principal:
AWS:
- !Sub arn:aws:iam::${AWS::AccountId}:root
Action: kms:*
Resource: "*"
- Sid: Allow use by repository account and pipeline
Effect: Allow
Principal:
AWS: "*"
Action:
- kms:DescribeKey
- kms:Encrypt
- kms:Decrypt
- kms:ReEncrypt*
- kms:GenerateDataKey
- kms:GenerateDataKeyWithoutPlaintext
Resource: "*"
Condition:
ArnEquals:
aws:PrincipalArn:
- !Sub arn:aws:iam::${AWS::AccountId}:root
- !Sub "arn:aws:iam::${RepositoryAccountId}:role/*"
- Sid: Allow attachment of persistent resources
Effect: Allow
Principal:
AWS: "*"
Action:
- kms:CreateGrant
- kms:ListGrants
- kms:RevokeGrant
Resource: "*"
Condition:
Bool:
kms:GrantIsForAWSResource: true
ArnEquals:
aws:PrincipalArn:
- !Sub arn:aws:iam::${AWS::AccountId}:root
- !Sub "arn:aws:iam::${RepositoryAccountId}:role/*"
PipelineExecutionRolePermissionPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: PipelineExecutionRolePermissions
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: 'iam:PassRole'
Resource: !GetAtt CloudFormationExecutionRole.Arn
- Effect: Allow
Action:
- "cloudformation:CreateChangeSet"
- "cloudformation:DescribeChangeSet"
- "cloudformation:ExecuteChangeSet"
- "cloudformation:DeleteStack"
- "cloudformation:DescribeStackEvents"
- "cloudformation:DescribeStacks"
- "cloudformation:GetTemplate"
- "cloudformation:GetTemplateSummary"
- "cloudformation:DescribeStackResource"
Resource: '*'
- Effect: Allow
Action:
- 's3:DeleteObject'
- 's3:GetObject*'
- 's3:PutObject*'
- 's3:GetBucket*'
- 's3:List*'
Resource:
- !Join [ '',[ !GetAtt ArtifactsBucket.Arn, '/*' ] ]
- !GetAtt ArtifactsBucket.Arn
Roles:
- !Ref PipelineExecutionRole
EventBus:
Type: AWS::Events::EventBus
Properties:
Name: !Ref EventBusName
EventBusPolicy:
Type: AWS::Events::EventBusPolicy
Properties:
EventBusName: !Ref EventBus
StatementId: RepositoryStateChange
Statement:
Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${RepositoryAccountId}:root
Action: events:PutEvents
Resource: !GetAtt EventBus.Arn
Outputs:
PipelineUser:
Description: ARN of the Pipeline IAM User
Value: !GetAtt PipelineUser.Arn
PipelineUserSecretKey:
Description: AWS Access Key and Secret Key of pipeline user.
Value: !Ref PipelineUserSecretKey
CloudFormationExecutionRole:
Description: ARN of the IAM Role(CloudFormationExecutionRole)
Value: !GetAtt CloudFormationExecutionRole.Arn
PipelineExecutionRole:
Description: ARN of the IAM Role(PipelineExecutionRole)
Value: !GetAtt PipelineExecutionRole.Arn
ArtifactsBucket:
Description: ARN of the Artifacts bucket
Value: !GetAtt ArtifactsBucket.Arn
ArtifactsBucketKey:
Description: ARN of the Artifacts bucket encryption key
Value: !GetAtt ArtifactsBucketKey.Arn
CodePipelineと関連リソース
PipelineAccountでCodePipelineと関連するリソースを作成します。
CloudFormationのパラメータには、先ほどのSAMパイプライン共通リソースで作成されたいくかのリソースを指定します。作成されたCloudFormationスタックの出力からコピーします。
- PipelineExecutionRole (共通リソースのPipelineExecutionRole)
- CloudFormationExecutionRole (共通リソースのCloudFormationExecutionRole)
- PipelineArtifactsBucket (共通リソースのArtifactsBucketのBucket名)
- PipelineArtifactsBucketKey (共通リソースのArtifactsBucketKey)
また、CodeCommitを設置したRepositoryAccountで作成したCloudFormationスタックから、以下のパラメータをコピーします。
- RepositoryAccountId (CodeCommit設置AWSアカウントID)
- CodeCommitRoleArn (CodeCommitリソースのCodeCommitRole)
このCloudFormationテンプレートはsam pipeline init --bootstrap
で自動作成されるcodepipeline.yamlを修正して作成しました。
AWSTemplateFormatVersion : '2010-09-09'
Description: >
This template deploys a CodePipeline with its required resources.
Parameters:
CodeCommitRepositoryName:
Type: String
Default: "zipcode"
GitBranch:
Type: String
Default: "main"
SamTemplate:
Type: String
Default: "template.yaml"
Region:
Type: String
Default: "ap-northeast-1"
TestingStackName:
Type: String
Default: "ZipCodeStaging"
ProdStackName:
Type: String
Default: "ZipCode"
PipelineExecutionRole:
Type: String
Default: "arn:aws:iam::123456789012:role/SamPipelineResource-PipelineExecutionRole-XXXXXXXXXXXX"
CloudFormationExecutionRole:
Type: String
Default: "arn:aws:iam::123456789012:role/SamPipelineResource-CloudFormationExecutionRole-XXXXXXXXXXXX"
PipelineArtifactsBucket:
Type: String
Default: "sampipelineresource-artifactsbucket-xxxxxxxxxxxxx"
PipelineArtifactsBucketKey:
Type: String
Default: "arn:aws:kms:ap-northeast-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
RepositoryAccountId:
Description: Account ID of CodeCommit
Type: String
Default: 987654321098
CodeCommitRoleArn:
Description: ARN of CodeCommit access Role
Type: String
Default: "arn:aws:iam::987654321098:role/ZipCodeRepository-CodeCommitRole-XXXXXXXXXXXX"
EventBusName:
Description: Event Bus name for CodeCommit state change notification
Type: String
Default: codecommit
BuildLogRetentionInDays:
Description: Retention days of build log on CloudWatch Logs LogGroup
Type: Number
Default: 1
Resources:
# ____
# / ___| ___ _ _ _ __ ___ ___
# \___ \ / _ \| | | | '__/ __/ _ \
# ___) | (_) | |_| | | | (_| __/
# |____/ \___/ \__,_|_| \___\___|
CloudWatchEventRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action: sts:AssumeRole
Policies:
-
PolicyName: cwe-pipeline-execution
PolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Action: codepipeline:StartPipelineExecution
Resource: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}"
CloudWatchEventRule:
Type: AWS::Events::Rule
Properties:
EventBusName: !Ref EventBusName
EventPattern:
source:
- aws.codecommit
detail-type:
- 'CodeCommit Repository State Change'
resources:
- !Sub "arn:${AWS::Partition}:codecommit:${AWS::Region}:${AWS::AccountId}:${CodeCommitRepositoryName}"
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- !Ref GitBranch
Targets:
- Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}"
RoleArn: !GetAtt CloudWatchEventRole.Arn
Id: codepipeline-AppPipeline
# ____ _ _ _
# | _ \(_)_ __ ___| (_)_ __ ___
# | |_) | | '_ \ / _ | | | '_ \ / _ \
# | __/| | |_) | __| | | | | | __/
# |_| |_| .__/ \___|_|_|_| |_|\___|
# |_|
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
ArtifactStore:
Location: !Ref PipelineArtifactsBucket
Type: S3
EncryptionKey:
Type: KMS
Id: !Ref PipelineArtifactsBucketKey
RoleArn: !GetAtt CodePipelineExecutionRole.Arn
RestartExecutionOnUpdate: true
Stages:
- Name: Source
Actions:
- Name: SourceCodeRepo
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: "1"
Configuration:
RepositoryName: !Ref CodeCommitRepositoryName
PollForSourceChanges: false
BranchName: !Ref GitBranch
OutputArtifacts:
- Name: SourceCodeAsZip
RunOrder: 1
RoleArn: !Ref CodeCommitRoleArn
- Name: UpdatePipeline
Actions:
- Name: CreateChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: "1"
Configuration:
ActionMode: CHANGE_SET_REPLACE
RoleArn: !GetAtt PipelineStackCloudFormationExecutionRole.Arn
StackName: !Ref AWS::StackName
ChangeSetName: !Sub ${AWS::StackName}-ChangeSet
TemplatePath: SourceCodeAsZip::codepipeline.yaml
Capabilities: CAPABILITY_NAMED_IAM
ParameterOverrides: !Sub |
{
"CodeCommitRepositoryName": "${CodeCommitRepositoryName}",
"GitBranch": "${GitBranch}",
"Region": "${Region}",
"TestingStackName": "${TestingStackName}",
"ProdStackName": "${ProdStackName}",
"PipelineExecutionRole": "${PipelineExecutionRole}",
"CloudFormationExecutionRole": "${CloudFormationExecutionRole}",
"PipelineArtifactsBucket": "${PipelineArtifactsBucket}",
"PipelineArtifactsBucketKey": "${PipelineArtifactsBucketKey}",
"RepositoryAccountId": "${RepositoryAccountId}",
"CodeCommitRoleArn": "${CodeCommitRoleArn}",
"EventBusName": "${EventBusName}",
"BuildLogRetentionInDays": "${BuildLogRetentionInDays}"
}
InputArtifacts:
- Name: SourceCodeAsZip
RunOrder: 1
- Name: ExecuteChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: "1"
Configuration:
ActionMode: CHANGE_SET_EXECUTE
RoleArn: !GetAtt PipelineStackCloudFormationExecutionRole.Arn
StackName: !Ref AWS::StackName
ChangeSetName: !Sub ${AWS::StackName}-ChangeSet
OutputArtifacts:
- Name: !Sub ${AWS::StackName}ChangeSet
RunOrder: 2
# Uncomment and modify the following step for running the unit-tests
# - Name: UnitTest
# Actions:
# - Name: UnitTest
# ActionTypeId:
# Category: Build
# Owner: AWS
# Provider: CodeBuild
# Version: "1"
# Configuration:
# ProjectName: !Ref CodeBuildProjectUnitTest
# InputArtifacts:
# - Name: SourceCodeAsZip
- Name: BuildAndPackage
Actions:
- Name: CodeBuild
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: "1"
Configuration:
ProjectName: !Ref CodeBuildProjectBuildAndPackage
InputArtifacts:
- Name: SourceCodeAsZip
OutputArtifacts:
- Name: BuildArtifactAsZip
- Name: DeployTest
Actions:
- Name: DeployTest
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: "1"
Configuration:
ProjectName: !Ref CodeBuildProjectDeploy
EnvironmentVariables: !Sub |
[
{"name": "ENV_TEMPLATE", "value": "packaged-test.yaml"},
{"name": "ENV_REGION", "value": "${Region}"},
{"name": "ENV_STACK_NAME", "value": "${TestingStackName}"},
{"name": "ENV_STAGE", "value": "Staging"},
{"name": "ENV_PIPELINE_EXECUTION_ROLE", "value": "${PipelineExecutionRole}"},
{"name": "ENV_CLOUDFORMATION_EXECUTION_ROLE", "value": "${CloudFormationExecutionRole}"},
{"name": "ENV_BUCKET", "value": "${PipelineArtifactsBucket}"}
]
InputArtifacts:
- Name: BuildArtifactAsZip
RunOrder: 1
# Uncomment the following step for running the integration tests
# - Name: IntegrationTest
# ActionTypeId:
# Category: Build
# Owner: AWS
# Provider: CodeBuild
# Version: "1"
# Configuration:
# ProjectName: !Ref CodeBuildProjectIntegrationTest
# InputArtifacts:
# - Name: SourceCodeAsZip
# RunOrder: 2
- Name: DeployProd
Actions:
# uncomment this to have a manual approval step before deployment to production
# - Name: ManualApproval
# ActionTypeId:
# Category: Approval
# Owner: AWS
# Provider: Manual
# Version: "1"
# RunOrder: 1
- Name: DeployProd
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: "1"
RunOrder: 2 # keeping run order as 2 in case manual approval is enabled
Configuration:
ProjectName: !Ref CodeBuildProjectDeploy
EnvironmentVariables: !Sub |
[
{"name": "ENV_TEMPLATE", "value": "packaged-prod.yaml"},
{"name": "ENV_REGION", "value": "${Region}"},
{"name": "ENV_STACK_NAME", "value": "${ProdStackName}"},
{"name": "ENV_STAGE", "value": "Production"},
{"name": "ENV_PIPELINE_EXECUTION_ROLE", "value": "${PipelineExecutionRole}"},
{"name": "ENV_CLOUDFORMATION_EXECUTION_ROLE", "value": "${CloudFormationExecutionRole}"},
{"name": "ENV_BUCKET", "value": "${PipelineArtifactsBucket}"}
]
InputArtifacts:
- Name: BuildArtifactAsZip
CodePipelineExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Policies:
- PolicyName: CrossAccountAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "sts:assumeRole"
Resource:
- !Sub "arn:aws:iam::${RepositoryAccountId}:role/*"
- PolicyName: Decrypt
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "kms:Decrypt"
Resource:
- !Ref PipelineArtifactsBucketKey
- PolicyName: CodePipelineAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "iam:PassRole"
Resource: "*"
- PolicyName: CodeCommitAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'codecommit:CancelUploadArchive'
- 'codecommit:GetBranch'
- 'codecommit:GetCommit'
- 'codecommit:GetUploadArchiveStatus'
- 'codecommit:UploadArchive'
Resource:
- !Sub "arn:${AWS::Partition}:codecommit:${AWS::Region}:${AWS::AccountId}:${CodeCommitRepositoryName}"
- PolicyName: CodePipelineCodeAndS3Bucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- s3:GetBucketAcl
- s3:GetBucketLocation
Effect: Allow
Resource: !Sub "arn:aws:s3:::${PipelineArtifactsBucket}"
- Action:
- "s3:GetObject"
- "s3:GetObjectVersion"
- "s3:PutObject"
Effect: Allow
Resource: !Sub "arn:aws:s3:::${PipelineArtifactsBucket}/*"
- PolicyName: CodePipelineCodeBuildAndCloudformationAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "codebuild:StartBuild"
- "codebuild:BatchGetBuilds"
Resource:
# Uncomment the line below to enable the unit-tests
# - !GetAtt CodeBuildProjectUnitTest.Arn
- !GetAtt CodeBuildProjectBuildAndPackage.Arn
# Uncomment the following step for running the integration tests
# - !GetAtt CodeBuildProjectIntegrationTest.Arn
- !GetAtt CodeBuildProjectDeploy.Arn
- Effect: Allow
Action:
- "cloudformation:CreateStack"
- "cloudformation:DescribeStacks"
- "cloudformation:DeleteStack"
- "cloudformation:UpdateStack"
- "cloudformation:CreateChangeSet"
- "cloudformation:ExecuteChangeSet"
- "cloudformation:DeleteChangeSet"
- "cloudformation:DescribeChangeSet"
- "cloudformation:SetStackPolicy"
- "cloudformation:SetStackPolicy"
- "cloudformation:ValidateTemplate"
Resource:
- !Sub "arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*"
PipelineStackCloudFormationExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Policies:
- PolicyName: GrantCloudFormationFullAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: '*'
Resource: '*'
# ____ _ ____ _ _ _
# / ___|___ __| | ___| __ ) _ _(_| | __| |
# | | / _ \ / _` |/ _ | _ \| | | | | |/ _` |
# | |__| (_) | (_| | __| |_) | |_| | | | (_| |
# \____\___/ \__,_|\___|____/ \__,_|_|_|\__,_|
CodeBuildServiceRole:
Type: AWS::IAM::Role
DependsOn:
- PipelineStackCloudFormationExecutionRole
Properties:
Tags:
- Key: Role
Value: aws-sam-pipeline-codebuild-service-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Policies:
- PolicyName: CodeBuildLogs
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource:
- !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*"
- PolicyName: CodeBuildArtifactsBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:GetObject"
- "s3:GetObjectVersion"
- "s3:PutObject"
Resource:
- !Sub "arn:${AWS::Partition}:s3:::${PipelineArtifactsBucket}/*"
- PolicyName: AssumeStagePipExecutionRoles
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Resource: "*"
Condition:
StringEquals:
aws:ResourceTag/Role: pipeline-execution-role
# Uncomment and modify the following step for running the unit-tests
# CodeBuildProjectUnitTest:
# Type: AWS::CodeBuild::Project
# Properties:
# Artifacts:
# Type: CODEPIPELINE
# Environment:
# Type: LINUX_CONTAINER
# ComputeType: BUILD_GENERAL1_SMALL
# Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
# ServiceRole: !GetAtt CodeBuildServiceRole.Arn
# Source:
# Type: CODEPIPELINE
# BuildSpec: pipeline/buildspec_unit_test.yml
# LogsConfig:
# CloudWatchLogs:
# GroupName: !Ref UnitTestLogGroup
# Status: ENABLED
CodeBuildProjectBuildAndPackage:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
PrivilegedMode: true
EnvironmentVariables:
- Name: SAM_TEMPLATE
Value: !Ref SamTemplate
- Name: TESTING_REGION
Value: !Ref Region
- Name: PROD_REGION
Value: !Ref Region
- Name: TESTING_PIPELINE_EXECUTION_ROLE
Value: !Ref PipelineExecutionRole
- Name: PROD_PIPELINE_EXECUTION_ROLE
Value: !Ref PipelineExecutionRole
- Name: TESTING_ARTIFACT_BUCKET
Value: !Ref PipelineArtifactsBucket
- Name: PROD_ARTIFACT_BUCKET
Value: !Ref PipelineArtifactsBucket
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: pipeline/buildspec_build_package.yml
LogsConfig:
CloudWatchLogs:
GroupName: !Ref BuildPackageLogGroup
Status: ENABLED
# Uncomment and modify the following step for running the integration tests
# CodeBuildProjectIntegrationTest:
# Condition: IsMainBranchPipeline
# Type: AWS::CodeBuild::Project
# Properties:
# Artifacts:
# Type: CODEPIPELINE
# Environment:
# Type: LINUX_CONTAINER
# ComputeType: BUILD_GENERAL1_SMALL
# Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
# ServiceRole: !GetAtt CodeBuildServiceRole.Arn
# Source:
# Type: CODEPIPELINE
# BuildSpec: pipeline/buildspec_integration_test.yml
# LogsConfig:
# CloudWatchLogs:
# GroupName: !Ref IntegrationTestLogGroup
# Status: ENABLED
CodeBuildProjectDeploy:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: pipeline/buildspec_deploy.yml
LogsConfig:
CloudWatchLogs:
GroupName: !Ref DeployLogGroup
Status: ENABLED
BuildPackageLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/codebuild/${AWS::StackName}-BuildPackage"
RetentionInDays: !Ref BuildLogRetentionInDays
DeployLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/codebuild/${AWS::StackName}-Deploy"
RetentionInDays: !Ref BuildLogRetentionInDays
上に掲載したcodepipeline.yamlは長いので、オリジナルからの変更部分(diff -bu)の結果を掲載します。diffの出力も長いですが…
diffの結果が分かりやすいように、コメントアウトされているUnitTestとIntegrationテストの部分は削除してからdiffを実行しました。
@@ -6,59 +6,50 @@
CodeCommitRepositoryName:
Type: String
Default: "zipcode"
- MainGitBranch:
+ GitBranch:
Type: String
Default: "main"
SamTemplate:
Type: String
Default: "template.yaml"
- TestingRegion:
+ Region:
Type: String
Default: "ap-northeast-1"
TestingStackName:
Type: String
Default: "ZipCodeStaging"
- TestingPipelineExecutionRole:
- Type: String
- Default: "arn:aws:iam::123456789012:role/aws-sam-cli-managed-staging-PipelineExecutionRole-XXXXXXXXXXXXX"
- TestingCloudFormationExecutionRole:
- Type: String
- Default: "arn:aws:iam::123456789012:role/aws-sam-cli-managed-stagi-CloudFormationExecutionR-XXXXXXXXXXXXX"
- TestingArtifactBucket:
- Type: String
- Default: "aws-sam-cli-managed-staging-pipel-artifactsbucket-xxxxxxxxxxxxx"
- TestingImageRepository:
- Type: String
- Default: ""
- ProdRegion:
- Type: String
- Default: "ap-northeast-1"
ProdStackName:
Type: String
Default: "ZipCode"
- ProdPipelineExecutionRole:
+ PipelineExecutionRole:
Type: String
- Default: "arn:aws:iam::123456789012:role/aws-sam-cli-managed-producti-PipelineExecutionRole-XXXXXXXXXXXX"
- ProdCloudFormationExecutionExeRole:
+ Default: "arn:aws:iam::123456789012:role/SamPipelineResource-PipelineExecutionRole-XXXXXXXXXXXX"
+ CloudFormationExecutionRole:
Type: String
- Default: "arn:aws:iam::123456789012:role/aws-sam-cli-managed-produ-CloudFormationExecutionR-XXXXXXXXXXXXX"
- ProdArtifactBucket:
+ Default: "arn:aws:iam::123456789012:role/SamPipelineResource-CloudFormationExecutionRole-XXXXXXXXXXXX"
+ PipelineArtifactsBucket:
Type: String
- Default: "aws-sam-cli-managed-production-pi-artifactsbucket-xxxxxxxxxxxx"
- ProdImageRepository:
+ Default: "sampipelineresource-artifactsbucket-xxxxxxxxxxxxx"
+ PipelineArtifactsBucketKey:
Type: String
- Default: ""
- CodeStarConnectionArn:
+ Default: "arn:aws:kms:ap-northeast-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ RepositoryAccountId:
+ Description: Account ID of CodeCommit
Type: String
- Default: ""
- FeatureGitBranch:
+ Default: 987654321098
+ CodeCommitRoleArn:
+ Description: ARN of CodeCommit access Role
Type: String
- Default: ""
+ Default: "arn:aws:iam::987654321098:role/ZipCodeRepository-CodeCommitRole-XXXXXXXXXXXX"
+ EventBusName:
+ Description: Event Bus name for CodeCommit state change notification
+ Type: String
+ Default: codecommit
+ BuildLogRetentionInDays:
+ Description: Retention days of build log on CloudWatch Logs LogGroup
+ Type: Number
+ Default: 1
-Conditions:
- IsMainBranchPipeline: !Equals [!Ref FeatureGitBranch, ""]
- IsFeatureBranchPipeline: !Not [Condition: IsMainBranchPipeline]
-
Resources:
# ____
# / ___| ___ _ _ _ __ ___ ___
@@ -91,6 +82,7 @@
CloudWatchEventRule:
Type: AWS::Events::Rule
Properties:
+ EventBusName: !Ref EventBusName
EventPattern:
source:
- aws.codecommit
@@ -105,7 +97,7 @@
referenceType:
- branch
referenceName:
- - !If [IsFeatureBranchPipeline, !Ref FeatureGitBranch, !Ref MainGitBranch]
+ - !Ref GitBranch
Targets:
- Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}"
RoleArn: !GetAtt CloudWatchEventRole.Arn
@@ -123,6 +115,9 @@
ArtifactStore:
Location: !Ref PipelineArtifactsBucket
Type: S3
+ EncryptionKey:
+ Type: KMS
+ Id: !Ref PipelineArtifactsBucketKey
RoleArn: !GetAtt CodePipelineExecutionRole.Arn
RestartExecutionOnUpdate: true
Stages:
@@ -137,10 +132,11 @@
Configuration:
RepositoryName: !Ref CodeCommitRepositoryName
PollForSourceChanges: false
- BranchName: !If [IsFeatureBranchPipeline, !Ref FeatureGitBranch, !Ref MainGitBranch]
+ BranchName: !Ref GitBranch
OutputArtifacts:
- Name: SourceCodeAsZip
RunOrder: 1
+ RoleArn: !Ref CodeCommitRoleArn
- Name: UpdatePipeline
Actions:
- Name: CreateChangeSet
@@ -158,8 +154,19 @@
Capabilities: CAPABILITY_NAMED_IAM
ParameterOverrides: !Sub |
{
- "FeatureGitBranch": "${FeatureGitBranch}",
- "CodeStarConnectionArn": "${CodeStarConnectionArn}"
+ "CodeCommitRepositoryName": "${CodeCommitRepositoryName}",
+ "GitBranch": "${GitBranch}",
+ "Region": "${Region}",
+ "TestingStackName": "${TestingStackName}",
+ "ProdStackName": "${ProdStackName}",
+ "PipelineExecutionRole": "${PipelineExecutionRole}",
+ "CloudFormationExecutionRole": "${CloudFormationExecutionRole}",
+ "PipelineArtifactsBucket": "${PipelineArtifactsBucket}",
+ "PipelineArtifactsBucketKey": "${PipelineArtifactsBucketKey}",
+ "RepositoryAccountId": "${RepositoryAccountId}",
+ "CodeCommitRoleArn": "${CodeCommitRoleArn}",
+ "EventBusName": "${EventBusName}",
+ "BuildLogRetentionInDays": "${BuildLogRetentionInDays}"
}
InputArtifacts:
- Name: SourceCodeAsZip
@@ -179,24 +186,6 @@
- Name: !Sub ${AWS::StackName}ChangeSet
RunOrder: 2
- - !If
- - IsFeatureBranchPipeline
- - Name: BuildAndDeployFeatureStack
- Actions:
- - Name: CodeBuild
- ActionTypeId:
- Category: Build
- Owner: AWS
- Provider: CodeBuild
- Version: "1"
- Configuration:
- ProjectName: !Ref CodeBuildProjectBuildAndDeployFeature
- InputArtifacts:
- - Name: SourceCodeAsZip
- - !Ref AWS::NoValue
-
- - !If
- - IsMainBranchPipeline
- Name: BuildAndPackage
Actions:
- Name: CodeBuild
@@ -211,10 +200,7 @@
- Name: SourceCodeAsZip
OutputArtifacts:
- Name: BuildArtifactAsZip
- - !Ref AWS::NoValue
- - !If
- - IsMainBranchPipeline
- Name: DeployTest
Actions:
- Name: DeployTest
@@ -228,20 +214,16 @@
EnvironmentVariables: !Sub |
[
{"name": "ENV_TEMPLATE", "value": "packaged-test.yaml"},
- {"name": "ENV_REGION", "value": "${TestingRegion}"},
+ {"name": "ENV_REGION", "value": "${Region}"},
{"name": "ENV_STACK_NAME", "value": "${TestingStackName}"},
- {"name": "ENV_PIPELINE_EXECUTION_ROLE", "value": "${TestingPipelineExecutionRole}"},
- {"name": "ENV_CLOUDFORMATION_EXECUTION_ROLE", "value": "${TestingCloudFormationExecutionRole}"},
- {"name": "ENV_BUCKET", "value": "${TestingArtifactBucket}"},
- {"name": "ENV_IMAGE_REPOSITORY", "value": "${TestingImageRepository}"}
+ {"name": "ENV_STAGE", "value": "Staging"},
+ {"name": "ENV_PIPELINE_EXECUTION_ROLE", "value": "${PipelineExecutionRole}"},
+ {"name": "ENV_CLOUDFORMATION_EXECUTION_ROLE", "value": "${CloudFormationExecutionRole}"},
+ {"name": "ENV_BUCKET", "value": "${PipelineArtifactsBucket}"}
]
InputArtifacts:
- Name: BuildArtifactAsZip
RunOrder: 1
- - !Ref AWS::NoValue
-
- - !If
- - IsMainBranchPipeline
- Name: DeployProd
Actions:
- Name: DeployProd
@@ -256,90 +238,16 @@
EnvironmentVariables: !Sub |
[
{"name": "ENV_TEMPLATE", "value": "packaged-prod.yaml"},
- {"name": "ENV_REGION", "value": "${ProdRegion}"},
+ {"name": "ENV_REGION", "value": "${Region}"},
{"name": "ENV_STACK_NAME", "value": "${ProdStackName}"},
- {"name": "ENV_PIPELINE_EXECUTION_ROLE", "value": "${ProdPipelineExecutionRole}"},
- {"name": "ENV_CLOUDFORMATION_EXECUTION_ROLE", "value": "${ProdCloudFormationExecutionExeRole}"},
- {"name": "ENV_BUCKET", "value": "${ProdArtifactBucket}"},
- {"name": "ENV_IMAGE_REPOSITORY", "value": "${ProdImageRepository}"}
+ {"name": "ENV_STAGE", "value": "Production"},
+ {"name": "ENV_PIPELINE_EXECUTION_ROLE", "value": "${PipelineExecutionRole}"},
+ {"name": "ENV_CLOUDFORMATION_EXECUTION_ROLE", "value": "${CloudFormationExecutionRole}"},
+ {"name": "ENV_BUCKET", "value": "${PipelineArtifactsBucket}"}
]
InputArtifacts:
- Name: BuildArtifactAsZip
- - !Ref AWS::NoValue
- PipelineArtifactsBucket:
- Type: AWS::S3::Bucket
- DeletionPolicy: Retain
- UpdateReplacePolicy: Retain
- Properties:
- VersioningConfiguration:
- Status: Enabled
- LoggingConfiguration:
- DestinationBucketName:
- !Ref PipelineArtifactsLoggingBucket
- LogFilePrefix: "artifacts-logs"
- BucketEncryption:
- ServerSideEncryptionConfiguration:
- - ServerSideEncryptionByDefault:
- SSEAlgorithm: AES256
-
- PipelineArtifactsBucketPolicy:
- Type: AWS::S3::BucketPolicy
- Properties:
- Bucket: !Ref PipelineArtifactsBucket
- PolicyDocument:
- Statement:
- - Effect: "Deny"
- Action: "s3:*"
- Principal: "*"
- Resource:
- - !Sub "${PipelineArtifactsBucket.Arn}/*"
- - !GetAtt PipelineArtifactsBucket.Arn
- Condition:
- Bool:
- aws:SecureTransport: false
- - Action:
- - s3:*
- Effect: Allow
- Resource:
- - !Sub arn:${AWS::Partition}:s3:::${PipelineArtifactsBucket}
- - !Sub arn:${AWS::Partition}:s3:::${PipelineArtifactsBucket}/*
- Principal:
- AWS:
- - !GetAtt CodePipelineExecutionRole.Arn
-
- PipelineArtifactsLoggingBucket:
- Type: AWS::S3::Bucket
- DeletionPolicy: Retain
- UpdateReplacePolicy: Retain
- Properties:
- AccessControl: "LogDeliveryWrite"
- OwnershipControls:
- Rules:
- - ObjectOwnership: ObjectWriter
- VersioningConfiguration:
- Status: Enabled
- BucketEncryption:
- ServerSideEncryptionConfiguration:
- - ServerSideEncryptionByDefault:
- SSEAlgorithm: AES256
-
- PipelineArtifactsLoggingBucketPolicy:
- Type: AWS::S3::BucketPolicy
- Properties:
- Bucket: !Ref PipelineArtifactsLoggingBucket
- PolicyDocument:
- Statement:
- - Effect: "Deny"
- Action: "s3:*"
- Principal: "*"
- Resource:
- - !Sub "${PipelineArtifactsLoggingBucket.Arn}/*"
- - !GetAtt PipelineArtifactsLoggingBucket.Arn
- Condition:
- Bool:
- aws:SecureTransport: false
-
CodePipelineExecutionRole:
Type: AWS::IAM::Role
Properties:
@@ -353,6 +261,24 @@
Service:
- codepipeline.amazonaws.com
Policies:
+ - PolicyName: CrossAccountAccess
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action:
+ - "sts:assumeRole"
+ Resource:
+ - !Sub "arn:aws:iam::${RepositoryAccountId}:role/*"
+ - PolicyName: Decrypt
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action:
+ - "kms:Decrypt"
+ Resource:
+ - !Ref PipelineArtifactsBucketKey
- PolicyName: CodePipelineAccess
PolicyDocument:
Version: "2012-10-17"
@@ -382,17 +308,13 @@
- s3:GetBucketAcl
- s3:GetBucketLocation
Effect: Allow
- Resource:
- Fn::GetAtt:
- - PipelineArtifactsBucket
- - Arn
+ Resource: !Sub "arn:aws:s3:::${PipelineArtifactsBucket}"
- Action:
- "s3:GetObject"
- "s3:GetObjectVersion"
- "s3:PutObject"
Effect: Allow
- Resource:
- Fn::Sub: ${PipelineArtifactsBucket.Arn}/*
+ Resource: !Sub "arn:aws:s3:::${PipelineArtifactsBucket}/*"
- PolicyName: CodePipelineCodeBuildAndCloudformationAccess
PolicyDocument:
@@ -403,18 +325,8 @@
- "codebuild:StartBuild"
- "codebuild:BatchGetBuilds"
Resource:
- - !If
- - IsFeatureBranchPipeline
- - !GetAtt CodeBuildProjectBuildAndDeployFeature.Arn
- - !Ref AWS::NoValue
- - !If
- - IsMainBranchPipeline
- !GetAtt CodeBuildProjectBuildAndPackage.Arn
- - !Ref AWS::NoValue
- - !If
- - IsMainBranchPipeline
- !GetAtt CodeBuildProjectDeploy.Arn
- - !Ref AWS::NoValue
- Effect: Allow
Action:
- "cloudformation:CreateStack"
@@ -457,6 +369,8 @@
# \____\___/ \__,_|\___|____/ \__,_|_|_|\__,_|
CodeBuildServiceRole:
Type: AWS::IAM::Role
+ DependsOn:
+ - PipelineStackCloudFormationExecutionRole
Properties:
Tags:
- Key: Role
@@ -505,39 +419,7 @@
StringEquals:
aws:ResourceTag/Role: pipeline-execution-role
- CodeBuildProjectBuildAndDeployFeature:
- Condition: IsFeatureBranchPipeline
- Type: AWS::CodeBuild::Project
- Properties:
- Artifacts:
- Type: CODEPIPELINE
- Environment:
- Type: LINUX_CONTAINER
- ComputeType: BUILD_GENERAL1_SMALL
- Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
- PrivilegedMode: true
- EnvironmentVariables:
- - Name: SAM_TEMPLATE
- Value: !Ref SamTemplate
- - Name: TESTING_REGION
- Value: !Ref TestingRegion
- - Name: TESTING_PIPELINE_EXECUTION_ROLE
- Value: !Ref TestingPipelineExecutionRole
- - Name: TESTING_CLOUDFORMATION_EXECUTION_ROLE
- Value: !Ref TestingCloudFormationExecutionRole
- - Name: TESTING_ARTIFACT_BUCKET
- Value: !Ref TestingArtifactBucket
- - Name: TESTING_IMAGE_REPOSITORY
- Value: !Ref TestingImageRepository
- - Name: FEATURE_BRANCH_NAME
- Value: !Ref FeatureGitBranch
- ServiceRole: !GetAtt CodeBuildServiceRole.Arn
- Source:
- Type: CODEPIPELINE
- BuildSpec: pipeline/buildspec_feature.yml
-
CodeBuildProjectBuildAndPackage:
- Condition: IsMainBranchPipeline
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
@@ -551,28 +433,27 @@
- Name: SAM_TEMPLATE
Value: !Ref SamTemplate
- Name: TESTING_REGION
- Value: !Ref TestingRegion
+ Value: !Ref Region
- Name: PROD_REGION
- Value: !Ref ProdRegion
+ Value: !Ref Region
- Name: TESTING_PIPELINE_EXECUTION_ROLE
- Value: !Ref TestingPipelineExecutionRole
+ Value: !Ref PipelineExecutionRole
- Name: PROD_PIPELINE_EXECUTION_ROLE
- Value: !Ref ProdPipelineExecutionRole
+ Value: !Ref PipelineExecutionRole
- Name: TESTING_ARTIFACT_BUCKET
- Value: !Ref TestingArtifactBucket
+ Value: !Ref PipelineArtifactsBucket
- Name: PROD_ARTIFACT_BUCKET
- Value: !Ref ProdArtifactBucket
- - Name: TESTING_IMAGE_REPOSITORY
- Value: !Ref TestingImageRepository
- - Name: PROD_IMAGE_REPOSITORY
- Value: !Ref ProdImageRepository
+ Value: !Ref PipelineArtifactsBucket
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: pipeline/buildspec_build_package.yml
+ LogsConfig:
+ CloudWatchLogs:
+ GroupName: !Ref BuildPackageLogGroup
+ Status: ENABLED
CodeBuildProjectDeploy:
- Condition: IsMainBranchPipeline
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
@@ -585,3 +466,19 @@
Source:
Type: CODEPIPELINE
BuildSpec: pipeline/buildspec_deploy.yml
+ LogsConfig:
+ CloudWatchLogs:
+ GroupName: !Ref DeployLogGroup
+ Status: ENABLED
+
+ BuildPackageLogGroup:
+ Type: AWS::Logs::LogGroup
+ Properties:
+ LogGroupName: !Sub "/aws/codebuild/${AWS::StackName}-BuildPackage"
+ RetentionInDays: !Ref BuildLogRetentionInDays
+
+ DeployLogGroup:
+ Type: AWS::Logs::LogGroup
+ Properties:
+ LogGroupName: !Sub "/aws/codebuild/${AWS::StackName}-Deploy"
+ RetentionInDays: !Ref BuildLogRetentionInDays
ビルドスクリプトの修正
デプロイ時に現在のステージ(Staging/Production)を判断するために、デプロイで起動されるCodeBuild(CodeBuildProjectDeploy)実行時の環境変数にENV_STAGEを追加しています。この環境変数を参照するように、デフォルトのpipeline/buildspec_deploy.ymlにも下記の修正をしました。
template.yamlのStageパラメータにENV_STAGEの値が渡されます。
--- a/pipeline/buildspec_deploy.yml
+++ b/pipeline/buildspec_deploy.yml
@@ -10,6 +10,7 @@ phases:
commands:
- . ./assume-role.sh ${ENV_PIPELINE_EXECUTION_ROLE} deploy
- sam deploy --stack-name ${ENV_STACK_NAME}
+ --parameter-overrides "Stage=${ENV_STAGE}"
--template ${ENV_TEMPLATE}
--capabilities CAPABILITY_IAM
--region ${ENV_REGION}
まとめ
CodeCommitを別のAWSアカウントに設置する構成で、SAMのCI/CDパイプラインを構築できました。
CloudFormationテンプレートは汎用的に作成されていますので、今回のテンプレートをコピーして若干の修正をすることで、今後作成する新しいアプリケーションにも使いまわしていくことが可能です。